From 8142b32f3865eccd3331328e0d087f805d186ed5 Mon Sep 17 00:00:00 2001 From: Oleg Lobanov Date: Fri, 11 Sep 2020 16:53:37 +0200 Subject: [PATCH] feat: put selected files in the root of the archive (closes #1065) --- fileutils/file.go | 57 ++++++++++++++++++++++++++++++++++++++++++ fileutils/file_test.go | 46 ++++++++++++++++++++++++++++++++++ http/raw.go | 13 +++++++--- 3 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 fileutils/file_test.go diff --git a/fileutils/file.go b/fileutils/file.go index 00549584..5c0248df 100644 --- a/fileutils/file.go +++ b/fileutils/file.go @@ -3,6 +3,7 @@ package fileutils import ( "io" "os" + "path" "path/filepath" "github.com/spf13/afero" @@ -50,3 +51,59 @@ func CopyFile(fs afero.Fs, source, dest string) error { return nil } + +// CommonPrefix returns common directory path of provided files +func CommonPrefix(sep byte, paths ...string) string { + // Handle special cases. + switch len(paths) { + case 0: + return "" + case 1: + return path.Clean(paths[0]) + } + + // Note, we treat string as []byte, not []rune as is often + // done in Go. (And sep as byte, not rune). This is because + // most/all supported OS' treat paths as string of non-zero + // bytes. A filename may be displayed as a sequence of Unicode + // runes (typically encoded as UTF-8) but paths are + // not required to be valid UTF-8 or in any normalized form + // (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different + // file names. + c := []byte(path.Clean(paths[0])) + + // We add a trailing sep to handle the case where the + // common prefix directory is included in the path list + // (e.g. /home/user1, /home/user1/foo, /home/user1/bar). + // path.Clean will have cleaned off trailing / separators with + // the exception of the root directory, "/" (in which case we + // make it "//", but this will get fixed up to "/" bellow). + c = append(c, sep) + + // Ignore the first path since it's already in c + for _, v := range paths[1:] { + // Clean up each path before testing it + v = path.Clean(v) + string(sep) + + // Find the first non-common byte and truncate c + if len(v) < len(c) { + c = c[:len(v)] + } + for i := 0; i < len(c); i++ { + if v[i] != c[i] { + c = c[:i] + break + } + } + } + + // Remove trailing non-separator characters and the final separator + for i := len(c) - 1; i >= 0; i-- { + if c[i] == sep { + c = c[:i] + break + } + } + + return string(c) +} diff --git a/fileutils/file_test.go b/fileutils/file_test.go new file mode 100644 index 00000000..fd2b5119 --- /dev/null +++ b/fileutils/file_test.go @@ -0,0 +1,46 @@ +package fileutils + +import "testing" + +func TestCommonPrefix(t *testing.T) { + testCases := map[string]struct { + paths []string + want string + }{ + "same lvl": { + paths: []string{ + "/home/user/file1", + "/home/user/file2", + }, + want: "/home/user", + }, + "sub folder": { + paths: []string{ + "/home/user/folder", + "/home/user/folder/file", + }, + want: "/home/user/folder", + }, + "relative path": { + paths: []string{ + "/home/user/folder", + "/home/user/folder/../folder2", + }, + want: "/home/user", + }, + "no common path": { + paths: []string{ + "/home/user/folder", + "/etc/file", + }, + want: "", + }, + } + for name, tt := range testCases { + t.Run(name, func(t *testing.T) { + if got := CommonPrefix('/', tt.paths...); got != tt.want { + t.Errorf("CommonPrefix() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/http/raw.go b/http/raw.go index 4976252b..b340165a 100644 --- a/http/raw.go +++ b/http/raw.go @@ -11,6 +11,7 @@ import ( "github.com/mholt/archiver" "github.com/filebrowser/filebrowser/v2/files" + "github.com/filebrowser/filebrowser/v2/fileutils" "github.com/filebrowser/filebrowser/v2/users" ) @@ -97,7 +98,7 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) return rawDirHandler(w, r, d, file) }) -func addFile(ar archiver.Writer, d *data, path string) error { +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) { @@ -115,10 +116,12 @@ func addFile(ar archiver.Writer, d *data, path string) error { } defer file.Close() + filename := strings.TrimPrefix(path, commonPath) + filename = strings.TrimPrefix(filename, "/") err = ar.Write(archiver.File{ FileInfo: archiver.FileInfo{ FileInfo: info, - CustomName: strings.TrimPrefix(path, "/"), + CustomName: filename, }, ReadCloser: file, }) @@ -133,7 +136,7 @@ func addFile(ar archiver.Writer, d *data, path string) error { } for _, name := range names { - err = addFile(ar, d, filepath.Join(path, name)) + err = addFile(ar, d, filepath.Join(path, name), commonPath) if err != nil { return err } @@ -167,8 +170,10 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files. } defer ar.Close() + commonDir := fileutils.CommonPrefix('/', filenames...) + for _, fname := range filenames { - err = addFile(ar, d, fname) + err = addFile(ar, d, fname, commonDir) if err != nil { return http.StatusInternalServerError, err }