Compare commits
3
Commits
main
...
v0.6.5-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2616e2939 | ||
|
|
d4df17a766 | ||
|
|
8e8707a091 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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'."
|
||||
@@ -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
@@ -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
@@ -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,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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user