diff --git a/frontend/src/api/pub.js b/frontend/src/api/pub.js
index 5afd0913..b4d4a2dd 100644
--- a/frontend/src/api/pub.js
+++ b/frontend/src/api/pub.js
@@ -1,23 +1,47 @@
-import { fetchJSON, removePrefix } from './utils'
+import { fetchURL, removePrefix } from './utils'
 import { baseURL } from '@/utils/constants'
 
-export async function fetch(hash, password = "") {
-  return fetchJSON(`/api/public/share/${hash}`, {
+export async function fetch (url, password = "") {
+  url = removePrefix(url)
+
+  const res = await fetchURL(`/api/public/share${url}`, {
     headers: {'X-SHARE-PASSWORD': password},
   })
+
+  if (res.status === 200) {
+    let data = await res.json()
+    data.url = `/share${url}`
+
+    if (data.isDir) {
+      if (!data.url.endsWith('/')) data.url += '/'
+      data.items = data.items.map((item, index) => {
+        item.index = index
+        item.url = `${data.url}${encodeURIComponent(item.name)}`
+
+        if (item.isDir) {
+          item.url += '/'
+        }
+
+        return item
+      })
+    }
+
+    return data
+  } else {
+    throw new Error(res.status)
+  }
 }
 
 export function download(format, hash, token, ...files) {
   let url = `${baseURL}/api/public/dl/${hash}`
 
-  const prefix = `/share/${hash}`
   if (files.length === 1) {
-    url += removePrefix(files[0], prefix) + '?'
+    url += encodeURIComponent(files[0]) + '?'
   } else {
     let arg = ''
 
     for (let file of files) {
-      arg += removePrefix(file, prefix) + ','
+      arg += encodeURIComponent(file)  + ','
     }
 
     arg = arg.substring(0, arg.length - 1)
diff --git a/frontend/src/api/utils.js b/frontend/src/api/utils.js
index ffe7844c..4659a260 100644
--- a/frontend/src/api/utils.js
+++ b/frontend/src/api/utils.js
@@ -33,12 +33,8 @@ export async function fetchJSON (url, opts) {
   }
 }
 
-export function removePrefix (url, prefix) {
-  if (url.startsWith('/files')) {
-    url = url.slice(6)
-  } else if (prefix) {
-    url = url.replace(prefix, '')
-  }
+export function removePrefix (url) {
+  url = url.split('/').splice(2).join('/')
 
   if (url === '') url = '/'
   if (url[0] !== '/') url = '/' + url
diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue
index 8f4bb8ba..ccd3cd32 100644
--- a/frontend/src/components/files/ListingItem.vue
+++ b/frontend/src/components/files/ListingItem.vue
@@ -66,7 +66,7 @@ export default {
       return this.readOnly == undefined && this.user.perm.rename
     },
     canDrop () {
-      if (!this.isDir || this.readOnly == undefined) return false
+      if (!this.isDir || this.readOnly !== undefined) return false
 
       for (let i of this.selected) {
         if (this.req.items[i].url === this.url) {
@@ -78,7 +78,11 @@ export default {
     },
     thumbnailUrl () {
       const path = this.url.replace(/^\/files\//, '')
-      return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
+
+      // reload the image when the file is replaced
+      const key = Date.parse(this.modified)
+
+      return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true&k=${key}`
     },
     isThumbsEnabled () {
       return enableThumbs
diff --git a/frontend/src/components/prompts/Info.vue b/frontend/src/components/prompts/Info.vue
index 2265b85c..0a3e8f21 100644
--- a/frontend/src/components/prompts/Info.vue
+++ b/frontend/src/components/prompts/Info.vue
@@ -63,7 +63,7 @@ export default {
         return moment(this.req.modified).fromNow()
       }
 
-      return moment(this.req.items[this.selected[0]]).fromNow()
+      return moment(this.req.items[this.selected[0]].modified).fromNow()
     },
     name: function () {
       return this.selectedCount === 0 ? this.req.name : this.req.items[this.selected[0]].name
diff --git a/frontend/src/css/styles.css b/frontend/src/css/styles.css
index 3b2df8d7..80138364 100644
--- a/frontend/src/css/styles.css
+++ b/frontend/src/css/styles.css
@@ -123,15 +123,21 @@
   color: #fff;
 }
 
-#previewer .action i {
+#previewer header > .action i {
   color: #fff;
 }
 
-#previewer .action:hover {
+@media (min-width: 738px) {
+  #previewer header #dropdown .action i {
+    color: #fff;
+  }
+}
+
+#previewer header .action:hover {
   background-color: rgba(255, 255, 255, 0.3)
 }
 
-#previewer .action span {
+#previewer header .action span {
   display: none;
 }
 
diff --git a/frontend/src/utils/buttons.js b/frontend/src/utils/buttons.js
index 8536b813..9f699db3 100644
--- a/frontend/src/utils/buttons.js
+++ b/frontend/src/utils/buttons.js
@@ -6,6 +6,10 @@ function loading (button) {
     return
   }
 
+  if (el.innerHTML == 'autorenew' || el.innerHTML == 'done') {
+    return
+  }
+
   el.dataset.icon = el.innerHTML
   el.style.opacity = 0
 
diff --git a/frontend/src/views/Share.vue b/frontend/src/views/Share.vue
index e3897fad..383f494e 100644
--- a/frontend/src/views/Share.vue
+++ b/frontend/src/views/Share.vue
@@ -31,7 +31,7 @@
               <a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
             </div>
             <div class="share__box__element share__box__center">
-              <qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
+              <qrcode-vue :value="link" size="200" level="M"></qrcode-vue>
             </div>
         </div>
         <div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items">
@@ -122,7 +122,6 @@ export default {
   },
   data: () => ({
     error: null,
-    path: '',
     showLimit: 500,
     password: '',
     attemptedPasswordLogin: false,
@@ -158,10 +157,9 @@ export default {
       if (this.token !== ''){
         queryArg = `?token=${this.token}`
       }
-      return `${baseURL}/api/public/dl/${this.hash}${this.path}${queryArg}`
-    },
-    fullLink: function () {
-      return window.location.origin + this.link
+
+      const path = this.$route.path.split('/').splice(2).join('/')
+      return `${baseURL}/api/public/dl/${path}${queryArg}`
     },
     humanSize: function () {
       if (this.req.isDir) {
@@ -193,20 +191,19 @@ export default {
       this.setLoading(true)
       this.error = null
 
+      if (this.password !== ''){
+        this.attemptedPasswordLogin = true
+      }
+
+      let url = this.$route.path
+      if (url === '') url = '/'
+      if (url[0] !== '/') url = '/' + url
+
       try {
-        if (this.password !== ''){
-          this.attemptedPasswordLogin = true
-        }
-        let file = await api.fetch(encodeURIComponent(this.$route.params.pathMatch), this.password)
-        this.path = file.path
-        if (this.path.endsWith('/'))  this.path = this.path.slice(0, -1)
+        let file = await api.fetch(url, this.password)
 
         this.token = file.token || ''
-        if (file.isDir) file.items = file.items.map((item, index) => {
-          item.index = index
-          item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(item.name)}`
-          return item
-        })
+
         this.updateRequest(file)
         this.setLoading(false)
       } catch (e) {
@@ -228,7 +225,7 @@ export default {
     },
     download () {
       if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
-        api.download(null, this.hash, this.token, this.req.items[this.selected[0]].url)
+        api.download(null, this.hash, this.token, this.req.items[this.selected[0]].path)
         return
       }
 
@@ -240,7 +237,7 @@ export default {
           let files = []
 
           for (let i of this.selected) {
-            files.push(this.req.items[i].url)
+            files.push(this.req.items[i].path)
           }
 
           api.download(format, this.hash, this.token, ...files)
diff --git a/frontend/src/views/files/Listing.vue b/frontend/src/views/files/Listing.vue
index c14462df..de67d37b 100644
--- a/frontend/src/views/files/Listing.vue
+++ b/frontend/src/views/files/Listing.vue
@@ -588,8 +588,12 @@ export default {
 
           let files = []
 
-          for (let i of this.selected) {
-            files.push(this.req.items[i].url)
+          if (this.selectedCount > 0) {
+            for (let i of this.selected) {
+              files.push(this.req.items[i].url)
+            }
+          } else {
+            files.push(this.$route.path)
           }
 
           api.download(format, ...files)
diff --git a/frontend/src/views/files/Preview.vue b/frontend/src/views/files/Preview.vue
index 21cb8ccd..495f1244 100644
--- a/frontend/src/views/files/Preview.vue
+++ b/frontend/src/views/files/Preview.vue
@@ -102,10 +102,13 @@ export default {
       return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
     },
     previewUrl () {
+      // reload the image when the file is replaced
+      const key = Date.parse(this.req.modified)
+
       if (this.req.type === 'image' && !this.fullSize) {
-        return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
+        return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}&k=${key}`
       }
-      return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
+      return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}&k=${key}`
     },
     raw () {
       return `${this.previewUrl}&inline=true`
diff --git a/http/http.go b/http/http.go
index 135fa05c..80c7e92c 100644
--- a/http/http.go
+++ b/http/http.go
@@ -54,8 +54,8 @@ func NewHandler(
 
 	api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
 	api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler(fileCache), "/api/resources")).Methods("DELETE")
-	api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST")
-	api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT")
+	api.PathPrefix("/resources").Handler(monkey(resourcePostHandler(fileCache), "/api/resources")).Methods("POST")
+	api.PathPrefix("/resources").Handler(monkey(resourcePutHandler, "/api/resources")).Methods("PUT")
 	api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH")
 
 	api.Path("/shares").Handler(monkey(shareListHandler, "/api/shares")).Methods("GET")
diff --git a/http/preview.go b/http/preview.go
index 0d956a51..562562ff 100644
--- a/http/preview.go
+++ b/http/preview.go
@@ -79,7 +79,7 @@ func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgServic
 		return errToStatus(err), err
 	}
 
-	cacheKey := previewCacheKey(file.Path, previewSize)
+	cacheKey := previewCacheKey(file.Path, file.ModTime.Unix(), previewSize)
 	cachedFile, ok, err := fileCache.Load(r.Context(), cacheKey)
 	if err != nil {
 		return errToStatus(err), err
@@ -133,6 +133,6 @@ func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgServic
 	return 0, nil
 }
 
-func previewCacheKey(fPath string, previewSize PreviewSize) string {
-	return fPath + previewSize.String()
+func previewCacheKey(fPath string, fTime int64, previewSize PreviewSize) string {
+	return fmt.Sprintf("%x%x%x", fPath, fTime, previewSize)
 }
diff --git a/http/public.go b/http/public.go
index c24be0d9..8dc400d3 100644
--- a/http/public.go
+++ b/http/public.go
@@ -16,7 +16,7 @@ import (
 
 var withHashFile = func(fn handleFunc) handleFunc {
 	return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
-		id, path := ifPathWithName(r)
+		id, ifPath := ifPathWithName(r)
 		link, err := d.store.Share.GetByHash(id)
 		if err != nil {
 			return errToStatus(err), err
@@ -47,21 +47,30 @@ var withHashFile = func(fn handleFunc) handleFunc {
 			return errToStatus(err), err
 		}
 
-		if file.IsDir {
-			// set fs root to the shared folder
-			d.user.Fs = afero.NewBasePathFs(d.user.Fs, filepath.Dir(link.Path))
+		// share base path
+		basePath := link.Path
 
-			file, err = files.NewFileInfo(files.FileOptions{
-				Fs:      d.user.Fs,
-				Path:    path,
-				Modify:  d.user.Perm.Modify,
-				Expand:  true,
-				Checker: d,
-				Token:   link.Token,
-			})
-			if err != nil {
-				return errToStatus(err), err
-			}
+		// file relative path
+		filePath := ""
+
+		if file.IsDir {
+			basePath = filepath.Dir(basePath)
+			filePath = ifPath
+		}
+
+		// set fs root to the shared file/folder
+		d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
+
+		file, err = files.NewFileInfo(files.FileOptions{
+			Fs:      d.user.Fs,
+			Path:    filePath,
+			Modify:  d.user.Perm.Modify,
+			Expand:  true,
+			Checker: d,
+			Token:   link.Token,
+		})
+		if err != nil {
+			return errToStatus(err), err
 		}
 
 		d.raw = file
diff --git a/http/raw.go b/http/raw.go
index 10f07bcd..5c224caa 100644
--- a/http/raw.go
+++ b/http/raw.go
@@ -108,8 +108,6 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
 })
 
 func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
-	// Checks are always done with paths with "/" as path separator.
-	path = strings.Replace(path, "\\", "/", -1)
 	if !d.Check(path) {
 		return nil
 	}
@@ -134,7 +132,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
 
 	if path != commonPath {
 		filename := strings.TrimPrefix(path, commonPath)
-		filename = strings.TrimPrefix(filename, "/")
+		filename = strings.TrimPrefix(filename, string(filepath.Separator))
 		err = ar.Write(archiver.File{
 			FileInfo: archiver.FileInfo{
 				FileInfo:   info,
@@ -175,20 +173,25 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.
 		return http.StatusInternalServerError, err
 	}
 
-	name := file.Name
-	if name == "." || name == "" {
-		name = "archive"
-	}
-	name += extension
-	w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
-
 	err = ar.Create(w)
 	if err != nil {
 		return http.StatusInternalServerError, err
 	}
 	defer ar.Close()
 
-	commonDir := fileutils.CommonPrefix('/', filenames...)
+	commonDir := fileutils.CommonPrefix(filepath.Separator, filenames...)
+
+	var name string
+	if len(filenames) > 1 {
+		name = "_" + filepath.Base(commonDir)
+	} else {
+		name = file.Name
+	}
+	if name == "." || name == "" {
+		name = "archive"
+	}
+	name += extension
+	w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
 
 	for _, fname := range filenames {
 		err = addFile(ar, d, fname, commonDir)
diff --git a/http/resource.go b/http/resource.go
index 9d036dca..2547a2ed 100644
--- a/http/resource.go
+++ b/http/resource.go
@@ -1,6 +1,7 @@
 package http
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -71,11 +72,9 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
 		}
 
 		// delete thumbnails
-		for _, previewSizeName := range PreviewSizeNames() {
-			size, _ := ParsePreviewSize(previewSizeName)
-			if err := fileCache.Delete(r.Context(), previewCacheKey(file.Path, size)); err != nil { //nolint:govet
-				return errToStatus(err), err
-			}
+		err = delThumbs(r.Context(), fileCache, file)
+		if err != nil {
+			return errToStatus(err), err
 		}
 
 		err = d.RunHook(func() error {
@@ -90,12 +89,59 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
 	})
 }
 
-var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
-	if !d.user.Perm.Create && r.Method == http.MethodPost {
-		return http.StatusForbidden, nil
-	}
+func resourcePostHandler(fileCache FileCache) handleFunc {
+	return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
+		if !d.user.Perm.Create || !d.Check(r.URL.Path) {
+			return http.StatusForbidden, nil
+		}
 
-	if !d.user.Perm.Modify && r.Method == http.MethodPut {
+		defer func() {
+			_, _ = io.Copy(ioutil.Discard, r.Body)
+		}()
+
+		// Directories creation on POST.
+		if strings.HasSuffix(r.URL.Path, "/") {
+			err := d.user.Fs.MkdirAll(r.URL.Path, 0775)
+			return errToStatus(err), err
+		}
+
+		file, err := files.NewFileInfo(files.FileOptions{
+			Fs:         d.user.Fs,
+			Path:       r.URL.Path,
+			Modify:     d.user.Perm.Modify,
+			Expand:     true,
+			ReadHeader: d.server.TypeDetectionByHeader,
+			Checker:    d,
+		})
+		if err == nil {
+			if r.URL.Query().Get("override") != "true" {
+				return http.StatusConflict, nil
+			}
+
+			err = delThumbs(r.Context(), fileCache, file)
+			if err != nil {
+				return errToStatus(err), err
+			}
+		}
+
+		err = d.RunHook(func() error {
+			info, _ := writeFile(d.user.Fs, r.URL.Path, r.Body)
+
+			etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
+			w.Header().Set("ETag", etag)
+			return nil
+		}, "upload", r.URL.Path, "", d.user)
+
+		if err != nil {
+			_ = d.user.Fs.RemoveAll(r.URL.Path)
+		}
+
+		return errToStatus(err), err
+	})
+}
+
+var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
+	if !d.user.Perm.Modify || !d.Check(r.URL.Path) {
 		return http.StatusForbidden, nil
 	}
 
@@ -103,55 +149,18 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
 		_, _ = io.Copy(ioutil.Discard, r.Body)
 	}()
 
-	// For directories, only allow POST for creation.
+	// Only allow PUT for files.
 	if strings.HasSuffix(r.URL.Path, "/") {
-		if r.Method == http.MethodPut {
-			return http.StatusMethodNotAllowed, nil
-		}
-
-		err := d.user.Fs.MkdirAll(r.URL.Path, 0775)
-		return errToStatus(err), err
-	}
-
-	if r.Method == http.MethodPost && r.URL.Query().Get("override") != "true" {
-		if _, err := d.user.Fs.Stat(r.URL.Path); err == nil {
-			return http.StatusConflict, nil
-		}
-	}
-
-	action := "upload"
-	if r.Method == http.MethodPut {
-		action = "save"
+		return http.StatusMethodNotAllowed, nil
 	}
 
 	err := d.RunHook(func() error {
-		dir, _ := path.Split(r.URL.Path)
-		err := d.user.Fs.MkdirAll(dir, 0775)
-		if err != nil {
-			return err
-		}
-
-		file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
-		if err != nil {
-			return err
-		}
-		defer file.Close()
-
-		_, err = io.Copy(file, r.Body)
-		if err != nil {
-			return err
-		}
-
-		// Gets the info about the file.
-		info, err := file.Stat()
-		if err != nil {
-			return err
-		}
+		info, _ := writeFile(d.user.Fs, r.URL.Path, r.Body)
 
 		etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
 		w.Header().Set("ETag", etag)
 		return nil
-	}, action, r.URL.Path, "", d.user)
+	}, "save", r.URL.Path, "", d.user)
 
 	if err != nil {
 		_ = d.user.Fs.RemoveAll(r.URL.Path)
@@ -165,6 +174,9 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
 	dst := r.URL.Query().Get("destination")
 	action := r.URL.Query().Get("action")
 	dst, err := url.QueryUnescape(dst)
+	if !d.Check(src) || !d.Check(dst) {
+		return http.StatusForbidden, nil
+	}
 	if err != nil {
 		return errToStatus(err), err
 	}
@@ -242,3 +254,41 @@ func addVersionSuffix(source string, fs afero.Fs) string {
 
 	return source
 }
+
+func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) {
+	dir, _ := path.Split(dst)
+	err := fs.MkdirAll(dir, 0775)
+	if err != nil {
+		return nil, err
+	}
+
+	file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	_, err = io.Copy(file, in)
+	if err != nil {
+		return nil, err
+	}
+
+	// Gets the info about the file.
+	info, err := file.Stat()
+	if err != nil {
+		return nil, err
+	}
+
+	return info, nil
+}
+
+func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
+	for _, previewSizeName := range PreviewSizeNames() {
+		size, _ := ParsePreviewSize(previewSizeName)
+		if err := fileCache.Delete(ctx, previewCacheKey(file.Path, file.ModTime.Unix(), size)); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}