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") }}
-
-
+
+
-
@@ -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 {