From cd3e111e39c9b49f925fd657de311b9ca5bf344b Mon Sep 17 00:00:00 2001 From: Graham Steffaniak <42989099+gtsteffaniak@users.noreply.github.com> Date: Wed, 8 Jan 2025 20:02:57 -0500 Subject: [PATCH] Beta/v0.3.6 release (#279) --- .github/workflows/regular-tests.yaml | 2 +- .github/workflows/release_beta.yaml | 81 +++++ .github/workflows/release_dev.yaml | 4 +- .../{release.yaml => release_stable.yaml} | 20 +- .github/workflows/{tag.yaml => tag_beta.yaml} | 4 +- .github/workflows/tag_stable.yaml | 31 ++ CHANGELOG.md | 16 + README.md | 6 +- backend/files/file.go | 2 +- backend/files/file_test.go | 2 - backend/fileutils/file.go | 59 +++- backend/http/public.go | 19 +- backend/http/raw.go | 15 +- backend/http/resource.go | 6 +- backend/http/router.go | 2 +- backend/http/static.go | 1 + backend/settings/structs.go | 1 + frontend/src/api/files.js | 28 +- frontend/src/api/share.js | 4 + frontend/src/api/utils.js | 3 +- frontend/src/api/utils.test.js | 8 +- frontend/src/components/ContextMenu.vue | 2 +- frontend/src/components/files/ListingItem.vue | 29 +- frontend/src/components/prompts/Copy.vue | 100 +++--- frontend/src/components/prompts/FileList.vue | 3 +- frontend/src/components/prompts/Move.vue | 86 +++-- frontend/src/components/prompts/NewDir.vue | 3 +- frontend/src/components/prompts/Share.vue | 7 +- frontend/src/css/listing.css | 84 +++-- frontend/src/router/index.ts | 9 +- frontend/src/store/getters.js | 11 - frontend/src/store/mutations.js | 31 +- frontend/src/utils/constants.js | 2 + frontend/src/utils/download.js | 24 ++ frontend/src/utils/upload.js | 2 + frontend/src/views/Files.vue | 7 +- frontend/src/views/Layout.vue | 34 +- frontend/src/views/Share.vue | 2 +- frontend/src/views/bars/Default.vue | 321 +----------------- frontend/src/views/files/ListingView.vue | 163 ++++++--- frontend/src/views/settings/User.vue | 1 - 41 files changed, 638 insertions(+), 597 deletions(-) create mode 100644 .github/workflows/release_beta.yaml rename .github/workflows/{release.yaml => release_stable.yaml} (77%) rename .github/workflows/{tag.yaml => tag_beta.yaml} (94%) create mode 100644 .github/workflows/tag_stable.yaml diff --git a/.github/workflows/regular-tests.yaml b/.github/workflows/regular-tests.yaml index fbcfb625..580dee9d 100644 --- a/.github/workflows/regular-tests.yaml +++ b/.github/workflows/regular-tests.yaml @@ -1,4 +1,4 @@ -name: dev tests +name: regular tests on: push: diff --git a/.github/workflows/release_beta.yaml b/.github/workflows/release_beta.yaml new file mode 100644 index 00000000..9ac1970a --- /dev/null +++ b/.github/workflows/release_beta.yaml @@ -0,0 +1,81 @@ +name: beta release + +on: + push: + branches: + - "beta/v[0-9]+.[0-9]+.[0-9]+" + +permissions: + contents: write + +jobs: + create_release_tag: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT }} + - name: Extract branch name + shell: bash + run: | + original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT + transformed_branch=$(echo "$original_branch" | sed 's/^beta\/v/beta_v/') + echo "tag_name=$transformed_branch" >> $GITHUB_OUTPUT + id: extract_branch + - uses: actions/setup-go@v5 + with: + go-version: 'stable' + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + target_commitish: ${{ steps.extract_branch.outputs.branch_name }} + token: ${{ secrets.PAT }} + tag_name: ${{ steps.extract_branch.outputs.tag_name }} + prerelease: false # change this to false when stable gets released + make_latest: true # change this to false when stable gets released + draft: false + generate_release_notes: true + name: ${{ steps.extract_branch.outputs.tag_name }} + + push_release_to_registry: + name: Push release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: gtstef/filebrowser + - name: Strip v from version number + id: modify-json + run: | + JSON="${{ steps.meta.outputs.tags }}" + # Use jq to remove 'v' from the version field + JSON=$(echo "$JSON" | sed 's/filebrowser:beta\/v/filebrowser:beta_v/') + echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + build-args: | + VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} + platforms: linux/amd64,linux/arm64,linux/arm/v7 + file: ./Dockerfile + push: true + tags: ${{ steps.modify-json.outputs.cleaned_tag }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release_dev.yaml b/.github/workflows/release_dev.yaml index c965d5b5..a9e36f39 100644 --- a/.github/workflows/release_dev.yaml +++ b/.github/workflows/release_dev.yaml @@ -3,7 +3,7 @@ name: dev release on: push: branches: - - "dev_v[0-9]+.[0-9]+.[0-9]+" + - "dev/v[0-9]+.[0-9]+.[0-9]+" permissions: contents: write @@ -34,7 +34,7 @@ jobs: run: | JSON="${{ steps.meta.outputs.tags }}" # Use jq to remove 'v' from the version field - JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/') + JSON=$(echo "$JSON" | sed 's/filebrowser:dev\/v/filebrowser:dev_v/') echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT - name: Build and push uses: docker/build-push-action@v6 diff --git a/.github/workflows/release.yaml b/.github/workflows/release_stable.yaml similarity index 77% rename from .github/workflows/release.yaml rename to .github/workflows/release_stable.yaml index cfd413c5..8ec64f15 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release_stable.yaml @@ -1,15 +1,15 @@ -name: version release +name: stable release on: push: branches: - - "v[0-9]+.[0-9]+.[0-9]+" + - "stable/v[0-9]+.[0-9]+.[0-9]+" permissions: contents: write jobs: - create_release: + create_release_tag: name: Create Release runs-on: ubuntu-latest steps: @@ -19,7 +19,11 @@ jobs: token: ${{ secrets.PAT }} - name: Extract branch name shell: bash - run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT + run: | + original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT + transformed_branch=$(echo "$original_branch" | sed 's/^stable\/v/stable_v/') + echo "tag_name=$transformed_branch" >> $GITHUB_OUTPUT id: extract_branch - uses: actions/setup-go@v5 with: @@ -27,13 +31,13 @@ jobs: - name: Create Release uses: softprops/action-gh-release@v2 with: - target_commitish: ${{ steps.extract_branch.outputs.branch }} + target_commitish: ${{ steps.extract_branch.outputs.branch_name }} token: ${{ secrets.PAT }} - tag_name: ${{ steps.extract_branch.outputs.branch }} + tag_name: ${{ steps.extract_branch.outputs.tag_name }} prerelease: false draft: false generate_release_notes: true - name: ${{ steps.extract_branch.outputs.branch }} + name: ${{ steps.extract_branch.outputs.tag_name }} push_release_to_registry: name: Push release @@ -60,7 +64,7 @@ jobs: run: | JSON="${{ steps.meta.outputs.tags }}" # Use jq to remove 'v' from the version field - JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/') + JSON=$(echo "$JSON" | sed 's/filebrowser:stable\/v/filebrowser:/') echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT - name: Build and push uses: docker/build-push-action@v6 diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag_beta.yaml similarity index 94% rename from .github/workflows/tag.yaml rename to .github/workflows/tag_beta.yaml index ee1588ef..1f4fda19 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag_beta.yaml @@ -1,9 +1,9 @@ -name: tag +name: beta tag update on: push: tags: - - "v*" + - "beta_v*" permissions: contents: write diff --git a/.github/workflows/tag_stable.yaml b/.github/workflows/tag_stable.yaml new file mode 100644 index 00000000..e3e7410c --- /dev/null +++ b/.github/workflows/tag_stable.yaml @@ -0,0 +1,31 @@ +name: stable tag update + +on: + push: + tags: + - "stable_v*" + +permissions: + contents: write + +jobs: + update_tag: + name: Update Release tag + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + - name: Install dependencies and build frontend + run: npm i && npm run build + working-directory: frontend + - name: Install UPX + run: sudo apt-get install -y upx + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + args: release --clean + workdir: backend + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5415c456..8ac55085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version). +## v0.3.6 + + **New Features** + - Adds "externalUrl" server config https://github.com/gtsteffaniak/filebrowser/issues/272 + + **Notes**: + - All views modes to show header bar for sorting. + - other small style changes + + **Bugfixes**: + - select and info bug after sorting https://github.com/gtsteffaniak/filebrowser/issues/277 + - downloading from shares with public user + - Ctrl and Shift key modifiers work on listing views as expected. + - copy/move file/folder error and show errors https://github.com/gtsteffaniak/filebrowser/issues/278 + - file move/copy context fix. + ## v0.3.5 **New Features** diff --git a/README.md b/README.md index 7b484161..6288932a 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,9 @@

-> [!Note] -> Starting with v0.3.3, configuration file mapping is different to support non-root user. Now, the default config file name is `config.yaml` and in docker the path is `/home/filebrowser/config.yaml` and `/home/filebrowser/`. Please read the usage below to properly update your config to point the new config location. (open an issue for any help needed) - > [!WARNING] -> There is no stable version yet. Always check release notes for bug fixes on functionality that may have been changed. If you notice any unexpected behavior -- please open an issue to have it fixed soon. +> There is no stable version yet. +> Stable release is planned for later this year after the following have been added: 1) multiple sources support 2) initial onboarding page 3) official automated docs website FileBrowser Quantum is a fork of the file browser open-source project with the following changes: diff --git a/backend/files/file.go b/backend/files/file.go index b3bb6801..1b5a72fd 100644 --- a/backend/files/file.go +++ b/backend/files/file.go @@ -189,7 +189,7 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error { index := GetIndex(source) // refresh info for source and dest err = index.RefreshFileInfo(FileOptions{ - Path: realsrc, + Path: filepath.Dir(realsrc), IsDir: isSrcDir, }) if err != nil { diff --git a/backend/files/file_test.go b/backend/files/file_test.go index 0e509a3a..22c49987 100644 --- a/backend/files/file_test.go +++ b/backend/files/file_test.go @@ -1,7 +1,6 @@ package files import ( - "fmt" "os" "path/filepath" "reflect" @@ -74,7 +73,6 @@ func Test_GetRealPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { realPath, isDir, _ := idx.GetRealPath(tt.paths...) - fmt.Println(realPath, trimPrefix) adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix) if tt.want.path != adjustedRealPath || tt.want.isDir != isDir { t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir) diff --git a/backend/fileutils/file.go b/backend/fileutils/file.go index fe43e3f6..e077093b 100644 --- a/backend/fileutils/file.go +++ b/backend/fileutils/file.go @@ -26,9 +26,25 @@ func MoveFile(src, dst string) error { return nil } -// CopyFile copies a file from source to dest and returns -// an error if any. +// CopyFile copies a file or directory from source to dest and returns an error if any. func CopyFile(source, dest string) error { + // Check if the source exists and whether it's a file or directory. + info, err := os.Stat(source) + if err != nil { + return err + } + + if info.IsDir() { + // If the source is a directory, copy it recursively. + return copyDirectory(source, dest) + } + + // If the source is a file, copy the file. + return copySingleFile(source, dest) +} + +// copySingleFile handles copying a single file. +func copySingleFile(source, dest string) error { // Open the source file. src, err := os.Open(source) if err != nil { @@ -36,7 +52,7 @@ func CopyFile(source, dest string) error { } defer src.Close() - // Makes the directory needed to create the dst file. + // Create the destination directory if needed. err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd if err != nil { return err @@ -68,6 +84,43 @@ func CopyFile(source, dest string) error { return nil } +// copyDirectory handles copying directories recursively. +func copyDirectory(source, dest string) error { + // Create the destination directory. + err := os.MkdirAll(dest, 0775) //nolint:gomnd + if err != nil { + return err + } + + // Read the contents of the source directory. + entries, err := os.ReadDir(source) + if err != nil { + return err + } + + // Iterate over each entry in the directory. + for _, entry := range entries { + srcPath := filepath.Join(source, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + + if entry.IsDir() { + // Recursively copy subdirectories. + err = copyDirectory(srcPath, destPath) + if err != nil { + return err + } + } else { + // Copy files. + err = copySingleFile(srcPath, destPath) + if err != nil { + return err + } + } + } + + return nil +} + // CommonPrefix returns the common directory path of provided files. func CommonPrefix(sep byte, paths ...string) string { // Handle special cases. diff --git a/backend/http/public.go b/backend/http/public.go index f2d0d8e3..d52f929e 100644 --- a/backend/http/public.go +++ b/backend/http/public.go @@ -15,7 +15,7 @@ import ( func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { file, ok := d.raw.(files.ExtendedFileInfo) if !ok { - return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo") + return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo") } file.Path = strings.TrimPrefix(file.Path, files.RootPaths["default"]) return renderJSON(w, r, file) @@ -28,23 +28,6 @@ func publicUserGetHandler(w http.ResponseWriter, r *http.Request) { if err != nil { http.Error(w, http.StatusText(status), status) } - -} - -func publicDlHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { - file, ok := d.raw.(files.ExtendedFileInfo) - if !ok { - return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo") - } - if d.user == nil { - return http.StatusUnauthorized, fmt.Errorf("failed to get user") - } - - if file.Type == "directory" { - return rawFilesHandler(w, r, d, []string{file.Path}) - } - - return rawFileHandler(w, r, file.FileInfo) } // health godoc diff --git a/backend/http/raw.go b/backend/http/raw.go index 4571c023..45ba80b2 100644 --- a/backend/http/raw.go +++ b/backend/http/raw.go @@ -45,13 +45,23 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, if !d.user.Perm.Download { return http.StatusAccepted, nil } + + filePrefix := "" + file, ok := d.raw.(files.ExtendedFileInfo) + if ok { + filePrefix = file.Path + } encodedFiles := r.URL.Query().Get("files") // Decode the URL-encoded path files, err := url.QueryUnescape(encodedFiles) if err != nil { return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) } - return rawFilesHandler(w, r, d, strings.Split(files, ",")) + fileList := strings.Split(files, ",") + for i, f := range fileList { + fileList[i] = filepath.Join(filePrefix, f) + } + return rawFilesHandler(w, r, d, fileList) } func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer) error { @@ -186,7 +196,8 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext, if baseDirName == "" || baseDirName == "/" { baseDirName = "download" } - downloadFileName := url.PathEscape(baseDirName + "." + algo) + downloadFileName := url.PathEscape(baseDirName + extension) + w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName) // Create the archive and stream it directly to the response if extension == ".zip" { diff --git a/backend/http/resource.go b/backend/http/resource.go index 5d5a324d..630b3c4f 100644 --- a/backend/http/resource.go +++ b/backend/http/resource.go @@ -283,10 +283,10 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont return errToStatus(err), err } if !d.user.Check(src) || !d.user.Check(dst) { - return http.StatusForbidden, nil + return http.StatusForbidden, fmt.Errorf("forbidden: user rules deny access to source or destination") } if dst == "/" || src == "/" { - return http.StatusForbidden, nil + return http.StatusForbidden, fmt.Errorf("forbidden: source or destination is attempting to modify root") } idx := files.GetIndex(source) @@ -307,7 +307,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont } // Permission for overwriting the file if overwrite && !d.user.Perm.Modify { - return http.StatusForbidden, nil + return http.StatusForbidden, fmt.Errorf("forbidden: user does not have permission to overwrite file") } err = d.RunHook(func() error { return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source) diff --git a/backend/http/router.go b/backend/http/router.go index 6a4fdfbc..b9bff04e 100644 --- a/backend/http/router.go +++ b/backend/http/router.go @@ -107,7 +107,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) { // Public routes api.HandleFunc("GET /public/publicUser", publicUserGetHandler) - api.HandleFunc("GET /public/dl", withHashFile(publicDlHandler)) + api.HandleFunc("GET /public/dl", withHashFile(rawHandler)) api.HandleFunc("GET /public/share", withHashFile(publicShareHandler)) // Settings routes diff --git a/backend/http/static.go b/backend/http/static.go index f28058ab..c008c2c4 100644 --- a/backend/http/static.go +++ b/backend/http/static.go @@ -63,6 +63,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT "EnableExec": config.Server.EnableExec, "ReCaptchaHost": config.Auth.Recaptcha.Host, "ExternalLinks": config.Frontend.ExternalLinks, + "ExternalUrl": strings.TrimSuffix(config.Server.ExternalUrl, "/"), } if config.Frontend.Files != "" { diff --git a/backend/settings/structs.go b/backend/settings/structs.go index 59db6722..388c2d25 100644 --- a/backend/settings/structs.go +++ b/backend/settings/structs.go @@ -53,6 +53,7 @@ type Server struct { UserHomeBasePath string `json:"userHomeBasePath"` CreateUserDir bool `json:"createUserDir"` Sources map[string]Source `json:"sources"` + ExternalUrl string `json:"externalUrl"` } type Source struct { diff --git a/frontend/src/api/files.js b/frontend/src/api/files.js index a5a2ae48..f767ce52 100644 --- a/frontend/src/api/files.js +++ b/frontend/src/api/files.js @@ -125,30 +125,38 @@ export async function post(url, content = "", overwrite = false, onupload) { } export async function moveCopy(items, action = "copy", overwrite = false, rename = false) { - let promises = []; let params = { overwrite: overwrite, action: action, rename: rename, - } + }; try { - for (let item of items) { + // Create an array of fetch calls + let promises = items.map((item) => { let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files")); let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files")); - let localParams = { ...params }; - localParams.destination = toPath; - localParams.from = fromPath; + let localParams = { ...params, destination: toPath, from: fromPath }; const apiPath = getApiPath("api/resources", localParams); - promises.push(fetch(apiPath, { method: "PATCH" })); - } - return promises; + return fetch(apiPath, { method: "PATCH" }).then((response) => { + if (!response.ok) { + // Throw an error if the fetch fails + return response.text().then((text) => { + throw new Error(`Failed to move/copy: ${text || response.statusText}`); + }); + } + return response; + }); + }); + // Await all promises and ensure errors propagate + await Promise.all(promises); } catch (err) { notify.showError(err.message || "Error moving/copying resources"); - throw err; + throw err; // Re-throw the error to propagate it back to the caller } } + export async function checksum(url, algo) { try { const data = await resourceAction(`${url}?checksum=${algo}`, "GET"); diff --git a/frontend/src/api/share.js b/frontend/src/api/share.js index 8108efee..74883b0e 100644 --- a/frontend/src/api/share.js +++ b/frontend/src/api/share.js @@ -1,6 +1,7 @@ import { fetchURL, fetchJSON, adjustedData } from "./utils"; import { notify } from "@/notify"; import { getApiPath } from "@/utils/url.js"; +import { externalUrl } from "@/utils/constants"; export async function list() { const apiPath = getApiPath("api/shares"); @@ -41,5 +42,8 @@ export async function create(path, password = "", expires = "", unit = "hours") } export function getShareURL(share) { + if (externalUrl) { + return externalUrl+getApiPath(`share/${share.hash}`); + } return window.origin+getApiPath(`share/${share.hash}`); } diff --git a/frontend/src/api/utils.js b/frontend/src/api/utils.js index 335f2034..705b8e1f 100644 --- a/frontend/src/api/utils.js +++ b/frontend/src/api/utils.js @@ -67,8 +67,7 @@ export function adjustedData(data, url) { // Combine folders and files into items data.items = [...(data.folders || []), ...(data.files || [])]; - data.items = data.items.map((item, index) => { - item.index = index; + data.items = data.items.map((item) => { item.url = `${data.url}${item.name}`; if (item.type === "directory") { item.url += "/"; diff --git a/frontend/src/api/utils.test.js b/frontend/src/api/utils.test.js index b54b81d2..6b627228 100644 --- a/frontend/src/api/utils.test.js +++ b/frontend/src/api/utils.test.js @@ -23,10 +23,10 @@ describe('adjustedData', () => { folders: [], files: [], items: [ - { name: "folder1", type: "directory", index: 0, url: "http://example.com/unit-testing/files/path/to/directory/folder1/" }, - { name: "folder2", type: "directory", index: 1, url: "http://example.com/unit-testing/files/path/to/directory/folder2/" }, - { name: "file1.txt", type: "file", index: 2, url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" }, - { name: "file2.txt", type: "file", index: 3, url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" }, + { name: "folder1", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder1/" }, + { name: "folder2", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder2/" }, + { name: "file1.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" }, + { name: "file2.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" }, ], }; diff --git a/frontend/src/components/ContextMenu.vue b/frontend/src/components/ContextMenu.vue index e4f7807f..e9a0ffff 100644 --- a/frontend/src/components/ContextMenu.vue +++ b/frontend/src/components/ContextMenu.vue @@ -109,7 +109,7 @@ export default { return state.user; }, centered() { - return getters.isMobile() || ( !this.posX || !this.posY ); + return getters.isMobile() || !this.posX || !this.posY; }, showContext() { if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) { diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue index 01de1d38..44f5f48e 100644 --- a/frontend/src/components/files/ListingItem.vue +++ b/frontend/src/components/files/ListingItem.vue @@ -179,7 +179,7 @@ export default { }, methods: { handleTouchMove(event) { - if (!state.isSafari) return + if (!state.isSafari) return; const touch = event.touches[0]; const deltaX = Math.abs(touch.clientX - this.touchStartX); const deltaY = Math.abs(touch.clientY - this.touchStartY); @@ -191,7 +191,7 @@ export default { } }, handleTouchEnd() { - if (!state.isSafari) return + if (!state.isSafari) return; this.cancelContext(); // Clear timeout this.isSwipe = false; // Reset swipe state }, @@ -214,8 +214,8 @@ export default { }, onRightClick(event) { event.preventDefault(); // Prevent default context menu - // If no items are selected, select the right-clicked item - if (!state.multiple) { + // If one or fewer items are selected, reset the selection + if (!state.multiple && getters.selectedCount() < 2) { mutations.resetSelected(); mutations.addSelected(this.index); } @@ -247,7 +247,9 @@ export default { getTime() { if (state.user.dateFormat) { // Truncate the fractional seconds to 3 digits (milliseconds) - const sanitizedString = this.modified.replace(/\.\d+/, (match) => match.slice(0, 4)); + const sanitizedString = this.modified.replace(/\.\d+/, (match) => + match.slice(0, 4) + ); // Parse the sanitized string into a Date object const date = new Date(sanitizedString); return date.toLocaleString(); @@ -333,7 +335,7 @@ export default { action(overwrite, rename); }, addSelected(event) { - if (!state.isSafari) return + if (!state.isSafari) return; const touch = event.touches[0]; this.touchStartX = touch.clientX; this.touchStartY = touch.clientY; @@ -357,7 +359,11 @@ export default { } } - if (!state.user.singleClick && getters.selectedCount() !== 0 && event.button === 0) { + if ( + !state.user.singleClick && + getters.selectedCount() !== 0 && + event.button === 0 + ) { event.preventDefault(); } setTimeout(() => { @@ -393,7 +399,12 @@ export default { return; } - if (!state.user.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) { + if ( + !state.user.singleClick && + !event.ctrlKey && + !event.metaKey && + !state.multiple + ) { mutations.resetSelected(); } mutations.addSelected(this.index); @@ -411,4 +422,4 @@ export default { .item { -webkit-touch-callout: none; /* Disable the default long press preview */ } - \ No newline at end of file + diff --git a/frontend/src/components/prompts/Copy.vue b/frontend/src/components/prompts/Copy.vue index 6767444f..5fb1338c 100644 --- a/frontend/src/components/prompts/Copy.vue +++ b/frontend/src/components/prompts/Copy.vue @@ -52,7 +52,8 @@ import FileList from "./FileList.vue"; import { filesApi } from "@/api"; import buttons from "@/utils/buttons"; import * as upload from "@/utils/upload"; -//import { notify } from "@/notify"; +import { removePrefix } from "@/utils/url"; +import { notify } from "@/notify"; export default { name: "copy", @@ -61,6 +62,7 @@ export default { return { current: window.location.pathname, dest: null, + items: [], }; }, computed: { @@ -71,57 +73,63 @@ export default { return mutations.closeHovers(); }, }, + mounted() { + for (let item of state.selected) { + this.items.push({ + from: state.req.items[item].url, + // add to: dest + name: state.req.items[item].name, + }); + } + }, methods: { copy: async function (event) { event.preventDefault(); - let items = []; + try { + // Define the action function + let action = async (overwrite, rename) => { + const loc = removePrefix(this.dest, "files"); + for (let item of this.items) { + item.to = loc + "/" + item.name; + } + buttons.loading("copy"); + await filesApi.moveCopy(this.items, "copy", overwrite, rename); + }; + // Fetch destination files + let dstResp = await filesApi.fetchFiles(this.dest); + let conflict = upload.checkConflict(this.items, dstResp.items); + let overwrite = false; + let rename = false; - // Create a new promise for each file. - for (let item of state.selected) { - items.push({ - from: state.req.items[item].url, - to: this.dest + encodeURIComponent(state.req.items[item].name), - name: state.req.items[item].name, - }); - } - - let action = async (overwrite, rename) => { - buttons.loading("copy"); - await filesApi.moveCopy(items, "copy", overwrite, rename); - this.$router.push({ path: this.dest }); - mutations.setReload(true); - }; - - if (state.route.path === this.dest) { + if (conflict) { + await new Promise((resolve, reject) => { + mutations.showHover({ + name: "replace-rename", + confirm: async (event, option) => { + overwrite = option == "overwrite"; + rename = option == "rename"; + event.preventDefault(); + try { + await action(overwrite, rename); + resolve(); // Resolve the promise if action succeeds + } catch (e) { + reject(e); // Reject the promise if an error occurs + } + }, + }); + }); + } else { + // Await the action call for non-conflicting cases + await action(overwrite, rename); + } mutations.closeHovers(); - action(false, true); - - return; + notify.showSuccess("Successfully copied file/folder, redirecting..."); + setTimeout(() => { + this.$router.push(this.dest); + }, 1000); + } catch (error) { + notify.error(error); } - - let dstItems = (await filesApi.fetchFiles(this.dest)).items; - let conflict = upload.checkConflict(items, dstItems); - - let overwrite = false; - let rename = false; - - if (conflict) { - mutations.showHover({ - name: "replace-rename", - confirm: (event, option) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - - event.preventDefault(); - mutations.closeHovers(); - action(overwrite, rename); - }, - }); - - return; - } - - action(overwrite, rename); }, }, }; diff --git a/frontend/src/components/prompts/FileList.vue b/frontend/src/components/prompts/FileList.vue index 14e6d515..5c23ad45 100644 --- a/frontend/src/components/prompts/FileList.vue +++ b/frontend/src/components/prompts/FileList.vue @@ -23,6 +23,7 @@ diff --git a/frontend/src/views/files/ListingView.vue b/frontend/src/views/files/ListingView.vue index 324b2609..e8fa4aba 100644 --- a/frontend/src/views/files/ListingView.vue +++ b/frontend/src/views/files/ListingView.vue @@ -16,26 +16,64 @@ sentiment_dissatisfied {{ $t("files.lonely") }} - - + + -
+
-

+

{{ $t("files.name") }} {{ nameIcon }}

-

+

{{ $t("files.size") }} {{ sizeIcon }}

-

+

{{ $t("files.lastModified") }} {{ modifiedIcon }}

@@ -46,10 +84,23 @@

{{ $t("files.folders") }}

-
- +
+
@@ -57,14 +108,35 @@
- +
- - + +
@@ -93,6 +165,7 @@ export default { width: window.innerWidth, lastSelected: {}, // Add this to track the currently focused item contextTimeout: null, // added for safari context menu + ctrKeyPressed: false, }; }, watch: { @@ -196,7 +269,6 @@ export default { this.colunmsResize(); return state.user.viewMode; }, - selectedCount() { return state.selected.length; }, @@ -215,7 +287,8 @@ export default { window.addEventListener("keydown", this.keyEvent); window.addEventListener("scroll", this.scrollEvent); window.addEventListener("resize", this.windowsResize); - this.$el.addEventListener("click", this.clickClear); + window.addEventListener("click", this.clickClear); + window.addEventListener("keyup", this.clearCtrKey); // Adjust contextmenu listener based on browser if (state.isSafari) { @@ -250,7 +323,6 @@ export default { this.$el.removeEventListener("touchend", this.cancelContext); this.$el.removeEventListener("mouseup", this.cancelContext); this.$el.removeEventListener("touchmove", this.handleTouchMove); - } else { window.removeEventListener("contextmenu", this.openContext); } @@ -425,17 +497,22 @@ export default { }, 50); } }, + clearCtrKey(event) { + const { ctrlKey } = event; + if (!ctrlKey) { + this.ctrKeyPressed = false; + } + }, keyEvent(event) { const { key, ctrlKey, metaKey, which } = event; // Check if the key is alphanumeric const isAlphanumeric = /^[a-z0-9]$/i.test(key); - const noModifierKeys = !ctrlKey && !metaKey; - - if (isAlphanumeric && noModifierKeys && getters.currentPromptName() == null) { + const modifierKeys = ctrlKey || metaKey; + if (isAlphanumeric && !modifierKeys && getters.currentPromptName() == null) { this.alphanumericKeyPress(key); // Call the alphanumeric key press function return; } - if (noModifierKeys && getters.currentPromptName() != null) { + if (!modifierKeys && getters.currentPromptName() != null) { return; } // Handle the space bar key @@ -452,6 +529,13 @@ export default { } let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes let newPath = currentPath.substring(0, currentPath.lastIndexOf("/")); + + if (modifierKeys) { + if (!ctrlKey) { + this.ctrKeyPressed = true; + } + return; + } // Handle key events using a switch statement switch (key) { case "Enter": @@ -486,11 +570,6 @@ export default { event.preventDefault(); this.navigateKeboardArrows(key); break; - - default: - // Handle keys with ctrl or meta keys - if (!ctrlKey && !metaKey) return; - break; } const charKey = String.fromCharCode(which).toLowerCase(); @@ -513,8 +592,6 @@ export default { break; } }, - - // Helper method to select all files and directories selectAll() { for (let file of this.items.files) { if (state.selected.indexOf(file.index) === -1) { @@ -650,12 +727,18 @@ export default { action(false, false); }, colunmsResize() { - document.documentElement.style.setProperty('--item-width', `calc(${100 / this.numColumns}% - 1em)`); + document.documentElement.style.setProperty( + "--item-width", + `calc(${100 / this.numColumns}% - 1em)` + ); if (state.user.viewMode == "gallery") { - document.documentElement.style.setProperty('--item-height', `calc(${this.columnWidth / 25}em)`); + document.documentElement.style.setProperty( + "--item-height", + `calc(${this.columnWidth / 25}em)` + ); } else { - document.documentElement.style.setProperty('--item-height', `auto`); + document.documentElement.style.setProperty("--item-height", `auto`); } }, dragEnter() { @@ -814,9 +897,11 @@ export default { }, }); }, - clickClear() { + clickClear(event) { + // if control or shift is pressed, do not clear the selection + if (this.ctrKeyPressed || event.shiftKey) return; const sameAsBefore = state.selected == this.lastSelected; - if (sameAsBefore && !state.multiple) { + if (sameAsBefore && !state.multiple && getters.currentPromptName == null) { mutations.resetSelected(); } this.lastSelected = state.selected; diff --git a/frontend/src/views/settings/User.vue b/frontend/src/views/settings/User.vue index d251b7df..cb9ac323 100644 --- a/frontend/src/views/settings/User.vue +++ b/frontend/src/views/settings/User.vue @@ -110,7 +110,6 @@ export default { event.preventDefault(); try { if (this.isNew) { - const loc = await usersApi.create(this.userPayload); // Use the computed property this.$router.push({ path: "/settings", hash: "#users-main" }); notify.showSuccess(this.$t("settings.userCreated")); } else {