Compare commits

...
3 Commits
Author SHA1 Message Date
Graham Steffaniak d2616e2939 updated workflow 2025-04-01 11:05:11 -05:00
Graham Steffaniak d4df17a766 updated with tests (#468) 2025-04-01 11:04:18 -05:00
Graham Steffaniak 8e8707a091 updated with share download fix (#467) 2025-03-31 16:16:01 -05:00
9 changed files with 198 additions and 98 deletions

No files matched your search

+10 -10
View File
@@ -87,16 +87,16 @@ jobs:
echo "✅ Successfully merged '$BETA_BRANCH' into '$STABLE_BRANCH'."
- name: Delete beta branch after successful merge
run: |
VERSION="${{ github.event.inputs.version }}"
BETA_BRANCH="beta/v$VERSION"
# Delete beta branch locally and remotely
git branch -d "$BETA_BRANCH"
git push origin --delete "$BETA_BRANCH"
echo "✅ Successfully deleted '$BETA_BRANCH'."
# - name: Delete beta branch after successful merge
# run: |
# VERSION="${{ github.event.inputs.version }}"
# BETA_BRANCH="beta/v$VERSION"
#
# # Delete beta branch locally and remotely
# git branch -d "$BETA_BRANCH"
# git push origin --delete "$BETA_BRANCH"
#
# echo "✅ Successfully deleted '$BETA_BRANCH'."
- name: Create Pull Request to Main
uses: peter-evans/create-pull-request@v6
+10 -10
View File
@@ -87,13 +87,13 @@ jobs:
echo "✅ Successfully merged '$DEV_BRANCH' into '$BETA_BRANCH'."
- name: Delete dev branch after successful merge
run: |
VERSION="${{ github.event.inputs.version }}"
DEV_BRANCH="dev/v$VERSION"
# Delete dev branch locally and remotely
git branch -d "$DEV_BRANCH"
git push origin --delete "$DEV_BRANCH"
echo "✅ Successfully deleted '$DEV_BRANCH'."
# - name: Delete dev branch after successful merge
# run: |
# VERSION="${{ github.event.inputs.version }}"
# DEV_BRANCH="dev/v$VERSION"
#
# # Delete dev branch locally and remotely
# git branch -d "$DEV_BRANCH"
# git push origin --delete "$DEV_BRANCH"
#
# echo "✅ Successfully deleted '$DEV_BRANCH'."
+9
View File
@@ -2,6 +2,15 @@
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.6.5-beta
**Notes**:
- added more share and download tests
**BugFixes**:
- fix share download issue https://github.com/gtsteffaniak/filebrowser/issues/465
- fix content length size calculation issue when downloading multiple files.
## v0.6.4-beta
**BugFixes**:
+11 -2
View File
@@ -31,11 +31,20 @@ import (
func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
encodedFiles := r.URL.Query().Get("files")
// Decode the URL-encoded path
files, err := url.QueryUnescape(encodedFiles)
f, err := url.QueryUnescape(encodedFiles)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
fileList := strings.Split(files, "||")
fileInfo, ok := d.raw.(files.ExtendedFileInfo)
if !ok {
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
}
prefix := fileInfo.Path
fileList := []string{}
for _, file := range strings.Split(f, "||") {
fileList = append(fileList, fileInfo.Source+"::"+prefix+file)
}
return rawFilesHandler(w, r, d, fileList)
}
+61 -57
View File
@@ -3,6 +3,7 @@ package http
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"errors"
"fmt"
@@ -60,9 +61,13 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
}
source := splitFile[0]
path = splitFile[1]
userScope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
if d.share == nil && err != nil {
return fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
var err error
userScope := "/"
if d.user.Username != "publicUser" {
userScope, err = settings.GetScopeFromSourceName(d.user.Scopes, source)
if d.share == nil && err != nil {
return fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
}
}
idx := files.GetIndex(source)
@@ -184,9 +189,13 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
firstFileSource := splitFile[0]
firstFilePath := splitFile[1]
fileName := filepath.Base(firstFilePath)
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, firstFileSource)
if err != nil && d.user.Username != "publicUser" {
return http.StatusForbidden, err
var err error
userscope := "/"
if d.user.Username != "publicUser" {
userscope, err = settings.GetScopeFromSourceName(d.user.Scopes, firstFileSource)
if err != nil {
return http.StatusForbidden, err
}
}
idx := files.GetIndex(firstFileSource)
if idx == nil {
@@ -197,14 +206,14 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
return http.StatusInternalServerError, err
}
// Compute estimated download size
estimatedSize, err := computeArchiveSize(fileList, d)
if err != nil {
return http.StatusInternalServerError, err
}
// ** Single file download with Content-Length **
if len(fileList) == 1 && !isDir {
var estimatedSize int64
// Compute estimated download size
estimatedSize, err = computeArchiveSize(fileList, d)
if err != nil {
return http.StatusInternalServerError, err
}
fd, err2 := os.Open(realPath)
if err2 != nil {
return http.StatusInternalServerError, err2
@@ -247,7 +256,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
return http.StatusInternalServerError, errors.New("format not implemented")
}
baseDirName := filepath.Base(filepath.Dir(realPath))
baseDirName := filepath.Base(filepath.Dir(firstFilePath))
if baseDirName == "" || baseDirName == "/" {
baseDirName = "download"
}
@@ -256,21 +265,29 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
}
fileName = url.PathEscape(baseDirName + extension)
// Set headers for archive
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+fileName)
w.Header().Set("Content-Length", fmt.Sprintf("%d", estimatedSize))
var archiveData []byte
if extension == ".zip" {
archiveData, err = createZip(d, fileList...)
} else {
archiveData, err = createTarGz(d, fileList...)
}
if err != nil {
return http.StatusInternalServerError, err
}
sizeInMB := estimatedSize / 1024 / 1024
calculatedSize := len(archiveData)
sizeInMB := calculatedSize / 1024 / 1024
// if larger than 100 megabytes, log it
if sizeInMB > 100 {
logger.Debug(fmt.Sprintf("User %v is downloading large (%d MB) file: %v", d.user.Username, sizeInMB, fileName))
}
// Create archive and stream it
if extension == ".zip" {
err = createZip(w, d, fileList...)
} else {
err = createTarGz(w, d, fileList...)
}
// Set headers AFTER computing actual archive size
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+fileName)
w.Header().Set("Content-Length", fmt.Sprintf("%d", calculatedSize))
// Write archive data to response
_, err = w.Write(archiveData)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -291,9 +308,13 @@ func computeArchiveSize(fileList []string, d *requestContext) (int64, error) {
if idx == nil {
return 0, fmt.Errorf("source %s is not available", source)
}
userScope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
if d.share == nil && err != nil {
return 0, fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
var err error
userScope := "/"
if d.user.Username != "publicUser" {
userScope, err = settings.GetScopeFromSourceName(d.user.Scopes, source)
if d.share == nil && err != nil {
return 0, fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
}
}
_, isDir, err := idx.GetRealPath(userScope, path)
if err != nil {
@@ -308,53 +329,36 @@ func computeArchiveSize(fileList []string, d *requestContext) (int64, error) {
return estimatedSize, nil
}
func createZip(w io.Writer, d *requestContext, filenames ...string) error {
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
// Stream the response to avoid high memory usage
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
func createZip(d *requestContext, filenames ...string) ([]byte, error) {
var buf bytes.Buffer
zipWriter := zip.NewWriter(&buf)
// Check if we have exactly one directory
//flatten := len(filenames) == 1
for _, fname := range filenames {
err := addFile(fname, d, nil, zipWriter, false)
if err != nil {
logger.Error(fmt.Sprintf("Failed to add %s to ZIP: %v", fname, err))
return err
}
// Flush after adding each file
if f, ok := w.(http.Flusher); ok {
f.Flush()
return nil, err
}
}
return nil
zipWriter.Close()
return buf.Bytes(), nil
}
func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
gzWriter := gzip.NewWriter(w)
defer gzWriter.Close()
func createTarGz(d *requestContext, filenames ...string) ([]byte, error) {
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
tarWriter := tar.NewWriter(gzWriter)
defer tarWriter.Close()
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
// Check if we have exactly one directory
//flatten := len(filenames) == 1
for _, fname := range filenames {
err := addFile(fname, d, tarWriter, nil, false)
if err != nil {
logger.Error(fmt.Sprintf("Failed to add %s to TAR.GZ: %v", fname, err))
return err
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
return nil, err
}
}
tarWriter.Close()
gzWriter.Close()
return nil
return buf.Bytes(), nil
}
+1 -2
View File
@@ -1,5 +1,5 @@
import { adjustedData } from "./utils";
import { getApiPath, removePrefix, extractSourceFromPath } from "@/utils/url.js";
import { getApiPath, extractSourceFromPath } from "@/utils/url.js";
import { notify } from "@/notify";
// Fetch public share data
@@ -37,7 +37,6 @@ export function download(share, ...files) {
}
fileargs = fileargs.slice(0, -2); // remove trailing "||"
const params = {
"path": removePrefix(share.path, "share"),
"files": fileargs,
"hash": share.hash,
"token": share.token,
+16 -10
View File
@@ -22,13 +22,12 @@ export default function download() {
format: files.length ? "zip" : null,
};
// Perform download without opening a new window
publicApi.download(share, ...files);
startDownload(share, files, true);
return;
}
if (getters.isSingleFileSelected()) {
// Single file download without new window
filesApi.download(null, [getters.selectedDownloadUrl()]);
startDownload(null, [getters.selectedDownloadUrl()]);
return;
}
@@ -45,13 +44,20 @@ export default function download() {
} else {
files.push(state.route.path);
}
try {
// Initiate download without new window
filesApi.download(format, files);
notify.showSuccess("Download started");
} catch (e) {
notify.showError("Error downloading", e);
}
startDownload(format, files);
},
});
}
async function startDownload(config,files, isPublic) {
try {
if (isPublic) {
publicApi.download(config, files);
} else {
filesApi.download(config, files);
}
notify.showSuccess("Downloading...");
} catch (e) {
notify.showError("Error downloading", e);
}
}
@@ -32,3 +32,57 @@ test("root share path is valid", async ({ page, checkForErrors, context }) => {
await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /');
checkForErrors();
});
test("share download single file", async ({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
const shareHash = await page.evaluate(() => localStorage.getItem('shareHash'));
if (shareHash == "") {
throw new Error("Share hash not found in localStorage");
}
await page.goto("/share/" + shareHash + "/testdata/");
await expect(page).toHaveTitle("Graham's Filebrowser - Share - testdata");
await page.locator('a[aria-label="gray-sample.jpg"]').click({ button: "right" });
await page.locator('button[aria-label="Download"]').waitFor({ state: 'visible' });
await page.locator('button[aria-label="Download"]').click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Downloading...");
checkForErrors();
});
test("share download multiple files", async ({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
const shareHash = await page.evaluate(() => localStorage.getItem('shareHash'));
if (shareHash == "") {
throw new Error("Share hash not found in localStorage");
}
await page.goto("/share/" + shareHash + "/testdata/");
await expect(page).toHaveTitle("Graham's Filebrowser - Share - testdata");
await page.locator('a[aria-label="gray-sample.jpg"]').click({ button: "right" });
await page.locator('button[aria-label="Select multiple"]').waitFor({ state: 'visible' });
await page.locator('button[aria-label="Select multiple"]').click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Multiple Selection Enabled");
await page.locator('a[aria-label="20130612_142406.jpg"]').click();
await page.locator('a[aria-label="IMG_2578.JPG"]').click();
await page.locator('a[aria-label="gray-sample.jpg"]').click({ button: "right" });
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
await expect(page.locator('.selected-count-header')).toHaveText('3 selected');
await page.locator('button[aria-label="Download"]').waitFor({ state: 'visible' });
await page.locator('button[aria-label="Download"]').click();
const popup2 = page.locator('#popup-notification-content');
await popup2.waitFor({ state: 'visible' });
await expect(popup2).toHaveText("Downloading...");
checkForErrors();
});
+26 -7
View File
@@ -1,6 +1,6 @@
import { test as base, expect, Page } from "@playwright/test";
export const test = base.extend<{ checkForErrors: () => void }>({
export const test = base.extend<{ checkForErrors: (expectedConsoleErrors?: number, expectedApiErrors?: number) => void }>({
checkForErrors: async ({ page }, use) => {
const { checkForErrors } = setupErrorTracking(page);
await use(checkForErrors);
@@ -9,19 +9,38 @@ export const test = base.extend<{ checkForErrors: () => void }>({
// Error tracking function
export function setupErrorTracking(page: Page) {
const errors: string[] = [];
const consoleErrors: string[] = [];
const failedResponses: { url: string; status: number }[] = [];
// Track console errors
page.on("console", (message) => {
if (message.type() === "error") {
errors.push(message.text());
consoleErrors.push(message.text());
}
});
// Track failed API calls
page.on("response", (response) => {
if (!response.ok()) {
failedResponses.push({
url: response.url(),
status: response.status(),
});
}
});
return {
checkForErrors: () => {
if (errors.length > 0) {
console.error("Console Errors Detected:", errors);
checkForErrors: (expectedConsoleErrors = 0, expectedApiErrors = 0) => {
if (consoleErrors.length !== expectedConsoleErrors) {
console.error("Unexpected Console Errors:", consoleErrors);
}
expect(errors).toHaveLength(0);
if (failedResponses.length !== expectedApiErrors) {
console.error("Unexpected Failed API Calls:", failedResponses);
}
expect(consoleErrors).toHaveLength(expectedConsoleErrors);
expect(failedResponses).toHaveLength(expectedApiErrors);
},
};
}