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
+}