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 @@ {{ $t('buttons.download') }}
- +
@@ -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 +}