Compare commits

...
13 changed files with 144 additions and 72 deletions

No files matched your search

+17 -13
View File
@@ -50,20 +50,24 @@ jobs:
uses: docker/metadata-action@v5
with:
images: gtstef/filebrowser
- name: Modify tags (strip 'v' and add 'beta' if needed)
- name: Modify tags (strip 'v' and add 'beta'/'stable' if needed)
id: modify-json
run: |
JSON="${{ steps.meta.outputs.tags }}"
# Remove 'v' from version tags
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
# If the tag includes "beta", append "filebrowser:beta"
if echo "$JSON" | grep -q "beta"; then
JSON="$JSON,gtstef/filebrowser:beta"
fi
if echo "$JSON" | grep -q "stable"; then
JSON="$JSON,gtstef/filebrowser:stable"
fi
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
JSON='${{ steps.meta.outputs.json }}' # Assuming the JSON output is stored in steps.meta.outputs.json
# Process the JSON array using jq
MODIFIED_JSON=$(echo "$JSON" | jq '
.tags |= map(sub(":v"; ":")) |
if any(.tags[]; test("beta")) then .tags += ["gtstef/filebrowser:beta"] else . end |
if any(.tags[]; test("stable")) then .tags += ["gtstef/filebrowser:stable"] else . end
')
# Convert JSON to a comma-separated string for Docker tags
TAGS_CSV=$(echo "$MODIFIED_JSON" | jq -r '.tags | join(",")')
echo "$TAGS_CSV" > modified_tags.txt # Save for debugging
echo "cleaned_tag=$TAGS_CSV" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6
with:
@@ -75,4 +79,4 @@ jobs:
file: ./_docker/Dockerfile
push: true
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
+8
View File
@@ -2,6 +2,14 @@
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.5.4-beta
**BugFixes**:
- default scope share issue. @theryecatcher https://github.com/gtsteffaniak/filebrowser/pull/387
- drag and drop on empty folders https://github.com/gtsteffaniak/filebrowser/issues/361
- preview navigation issue https://github.com/gtsteffaniak/filebrowser/issues/372
- auth proxy password length error https://github.com/gtsteffaniak/filebrowser/issues/375
## v0.5.3-beta
**New Features**
+8 -7
View File
@@ -40,7 +40,9 @@ func withHashFileHelper(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
path := r.URL.Query().Get("path")
hash := r.URL.Query().Get("hash")
// Retrieve the user (using the public user by default)
data.user = &users.PublicUser
data.user.Scope = ""
// Get the file link by hash
link, err := store.Share.GetByHash(hash)
@@ -55,15 +57,16 @@ func withHashFileHelper(fn handleFunc) handleFunc {
return status, fmt.Errorf("could not authenticate share request")
}
}
// Retrieve the user (using the public user by default)
user := &users.PublicUser
// Set the scope to configured defaults
scope := settings.Config.UserDefaults.Scope
// Get file information with options
file, err := FileInfoFasterFunc(files.FileOptions{
Path: filepath.Join(user.Scope, link.Path+"/"+path),
Modify: user.Perm.Modify,
Path: filepath.Join(scope, link.Path+"/"+path),
Modify: data.user.Perm.Modify,
Expand: true,
Checker: user, // Call your checker function here
Checker: data.user, // Call your checker function here
})
file.Token = link.Token
if err != nil {
@@ -190,7 +193,6 @@ func wrapHandler(fn handleFunc) http.HandlerFunc {
// Call the actual handler function and get status code and error
status, err := fn(w, r, data)
// Handle the error case if there is one
if err != nil {
// Create an error response in JSON format
@@ -303,7 +305,6 @@ func setUserInResponseWriter(w http.ResponseWriter, user *users.User) {
// LoggingMiddleware logs each request and its status code.
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap the ResponseWriter to capture the status code.
+10 -8
View File
@@ -86,15 +86,17 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
}
func (st usersBackend) Save(user *users.User) error {
if len(user.Password) < settings.Config.Auth.Methods.PasswordAuth.MinLength {
return fmt.Errorf("password must be at least %d characters long", settings.Config.Auth.Methods.PasswordAuth.MinLength)
if settings.Config.Auth.Methods.PasswordAuth.Enabled {
if len(user.Password) < settings.Config.Auth.Methods.PasswordAuth.MinLength {
return fmt.Errorf("password must be at least %d characters long", settings.Config.Auth.Methods.PasswordAuth.MinLength)
}
pass, err := users.HashPwd(user.Password)
if err != nil {
return err
}
user.Password = pass
}
pass, err := users.HashPwd(user.Password)
if err != nil {
return err
}
user.Password = pass
err = st.db.Save(user)
err := st.db.Save(user)
if err == storm.ErrAlreadyExists {
return errors.ErrExist
}
+2 -1
View File
@@ -7,6 +7,7 @@
<div class="card-content">
<p>{{ $t("prompts.newDirMessage") }}</p>
<input
aria-label="New Folder Name"
class="input input--block"
type="text"
@keyup.enter="submit"
@@ -88,7 +89,7 @@ export default {
uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/");
await filesApi.post(uri);
await filesApi.post(uri, "", true);
if (this.redirect) {
this.$router.push({ path: uri });
} else if (!this.base) {
+11 -8
View File
@@ -1,20 +1,18 @@
import { state, mutations, getters } from "@/store"
import { state, mutations, getters } from "@/store";
import { filesApi } from "@/api";
import { notify } from "@/notify"
import { notify } from "@/notify";
import { removePrefix } from "@/utils/url.js";
import { publicApi } from "@/api";
export default function download() {
if (getters.currentView() === "share") {
let urlPath = getters.routePath("share");
// Step 1: Split the path by '/'
let parts = urlPath.split("/");
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
const hash = parts[1];
const subPath = "/" + parts.slice(2).join("/");
let files = [];
for (let i of state.selected) {
const dlfile = removePrefix(state.req.items[i].url, "share/"+hash);
const dlfile = removePrefix(state.req.items[i].url, "share/" + hash);
files.push(dlfile);
}
const share = {
@@ -23,14 +21,18 @@ export default function download() {
token: "",
format: files.length ? "zip" : null,
};
// Perform download without opening a new window
publicApi.download(share, ...files);
return
return;
}
if (getters.isSingleFileSelected()) {
// Single file download without new window
filesApi.download(null, [getters.selectedDownloadUrl()]);
return;
}
// Multiple files download with user confirmation
mutations.showHover({
name: "download",
confirm: (format) => {
@@ -44,10 +46,11 @@ export default function download() {
files.push(state.route.path);
}
try {
// Initiate download without new window
filesApi.download(format, files);
notify.showSuccess("download started");
notify.showSuccess("Download started");
} catch (e) {
notify.showError("error downloading", e);
notify.showError("Error downloading", e);
}
},
});
+25 -6
View File
@@ -1,7 +1,26 @@
export function getFileExtension(filename) {
const firstDotIndex = filename.indexOf('.');
if (firstDotIndex === -1) {
return filename; // No dot found, return the original filename
}
return filename.substring(firstDotIndex);
export function getFileExtension (filename) {
if (typeof filename !== 'string') {
return ''
}
const firstDotIndex = filename.indexOf('.')
// If no dot exists, return an empty string
if (firstDotIndex === -1) {
return ''
}
// Default: Get everything after the first dot
const firstDotExtension = filename.substring(firstDotIndex)
if (firstDotExtension === '.') {
return ""
}
// If it's 7 or fewer characters (including the dot), return it
if (firstDotExtension.length <= 7) {
return firstDotExtension
}
// Otherwise, return everything after the last dot
const lastDotIndex = filename.lastIndexOf('.')
return filename.substring(lastDotIndex)
}
+20
View File
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest';
import { getFileExtension } from './files.js';
describe('testSort', () => {
it('get extension from file', () => {
const tests = [
{input: "hi.txt", expected:".txt"},
{input: "hello world.exe", expected:".exe"},
{input: "Amazon.com - Order.pdf", expected:".pdf"},
{input: "file", expected:""},
{input: "file.", expected:""},
{input: "file.tar.gz", expected:".tar.gz"},
]
for (let i in tests) {
expect(getFileExtension(tests[i].input)).toEqual(tests[i].expected);
}
});
});
+2 -1
View File
@@ -112,9 +112,10 @@ export default {
try {
// Fetch initial data
let res = await filesApi.fetchFiles(getters.routePath());
console.log("fetched", res.name);
// If not a directory, fetch content
if (res.type != "directory") {
const content = !getters.onlyOfficeEnabled()
const content = !getters.onlyOfficeEnabled();
res = await filesApi.fetchFiles(getters.routePath(), content);
}
data = res;
+13 -6
View File
@@ -11,7 +11,13 @@
</h2>
</div>
<div v-else>
<div v-if="numDirs + numFiles == 0">
<div
id="listingView"
ref="listingView"
class="font-size-large"
:class="{ 'add-padding': isStickySidebar, [listingViewMode]: true }"
v-if="numDirs + numFiles == 0"
>
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t("files.lonely") }}</span>
@@ -36,7 +42,7 @@
v-else
id="listingView"
ref="listingView"
:class="{'add-padding': isStickySidebar,[listingViewMode]: true }"
:class="{ 'add-padding': isStickySidebar, [listingViewMode]: true }"
class="file-icons"
>
<div>
@@ -176,7 +182,6 @@ export default {
this.columnWidth = 250 + state.user.gallerySize * 50;
this.colunmsResize();
},
},
computed: {
isStickySidebar() {
@@ -805,7 +810,7 @@ export default {
if (el !== null && el.classList.contains("item") && el.dataset.dir === "true") {
path = el.__vue__.url;
items = state.req.items
items = state.req.items;
}
const conflict = upload.checkConflict(uploadFiles, items);
@@ -824,7 +829,7 @@ export default {
}
mutations.setReload(true);
} catch {
console.log("failed to upload files")
console.log("failed to upload files");
}
},
async uploadInput(event) {
@@ -940,5 +945,7 @@ export default {
.add-padding {
padding-left: 0.5em;
}
.font-size-large h2 {
font-size: 2em !important;
}
</style>
+10 -17
View File
@@ -115,13 +115,12 @@ export default {
previousRaw: "",
nextRaw: "",
currentPrompt: null, // Replaces Vuex getter `currentPrompt`
oldReq: {}, // Replace with your actual initial state
subtitlesList: [],
};
},
computed: {
raw() {
return filesApi.getDownloadURL(state.req.path);
return filesApi.getDownloadURL(state.req.path, true);
},
isDarkMode() {
return getters.isDarkMode();
@@ -149,7 +148,7 @@ export default {
},
},
watch: {
$route() {
req() {
if (!getters.isLoggedIn()) {
return;
}
@@ -159,7 +158,6 @@ export default {
},
async mounted() {
window.addEventListener("keydown", this.key);
this.listing = this.oldReq.items;
this.subtitlesList = await this.subtitles();
this.updatePreview();
},
@@ -186,7 +184,6 @@ export default {
src: vttURL,
});
}
console.log(subs);
return subs;
},
getSimpleType(mimetype) {
@@ -242,12 +239,12 @@ export default {
if (this.$refs.player && this.$refs.player.paused && !this.$refs.player.ended) {
this.autoPlay = false;
}
this.name = state.req.name;
if (!this.listing) {
const path = url.removeLastDir(state.route.path);
const path = url.removeLastDir(getters.routePath());
const res = await filesApi.fetchFiles(path);
this.listing = res.items;
}
this.name = state.req.name;
this.previousLink = "";
this.nextLink = "";
const path = state.req.path;
@@ -263,20 +260,16 @@ export default {
for (let j = i - 1; j >= 0; j--) {
let composedListing = this.listing[j];
composedListing.path = directoryPath + "/" + composedListing.name;
if (mediaTypes.includes(composedListing.type.split("/")[0])) {
this.previousLink = composedListing.url;
this.previousRaw = this.prefetchUrl(composedListing);
break;
}
this.previousLink = composedListing.url;
this.previousRaw = this.prefetchUrl(composedListing);
break;
}
for (let j = i + 1; j < this.listing.length; j++) {
let composedListing = this.listing[j];
composedListing.path = directoryPath + "/" + composedListing.name;
if (mediaTypes.includes(composedListing.type.split("/")[0])) {
this.nextLink = composedListing.url;
this.nextRaw = this.prefetchUrl(composedListing);
break;
}
this.nextLink = composedListing.url;
this.nextRaw = this.prefetchUrl(composedListing);
break;
}
return;
}
+18 -4
View File
@@ -24,7 +24,7 @@ test("info from search", async ({ page, context }) => {
await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz');
})
test("copy from listing 2x", async ({ page, context }) => {
test("2x copy from listing to new folder", async ({ page, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
@@ -44,18 +44,32 @@ test("copy from listing 2x", async ({ page, context }) => {
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
// verify exists and copy again
await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' });
// create new directory
// Ensure #listingView is visible
await page.locator('#listingView').click({ button: "right" });
await page.locator('button[aria-label="New folder"]').waitFor({ state: 'visible' });
await page.locator('button[aria-label="New folder"]').click();
await page.locator('input[aria-label="New Folder Name"]').waitFor({ state: 'visible' });
await page.locator('input[aria-label="New Folder Name"]').fill('newfolder');
await page.locator('button[aria-label="Create"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - newfolder");
await page.goBack();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - myfolder");
await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" });
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
await page.locator('button[aria-label="Copy file"]').click();
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /myfolder/');
await page.locator('li[aria-label="testdata"]').click();
await page.locator('li[aria-label="newfolder"]').click();
await page.locator('button[aria-label="Copy"]').click();
const popup2 = page.locator('#popup-notification-content');
await popup2.waitFor({ state: 'visible' });
await expect(popup2).toHaveText("Successfully copied file/folder, redirecting...");
await page.waitForURL('**/testdata/');
await expect(page).toHaveTitle("Graham's Filebrowser - Files - testdata");
await page.waitForURL('**/newfolder/');
await expect(page).toHaveTitle("Graham's Filebrowser - Files - newfolder");
})
test("delete file", async ({ page, context }) => {
-1
View File
@@ -43,7 +43,6 @@ test("navigating images", async ({ page, context }) => {
await page.mouse.move(100, 100);
await page.locator('button[aria-label="Next"]').waitFor({ state: 'visible' });
await page.locator('button[aria-label="Next"]').click();
// went to next image
await expect(page).toHaveTitle("Graham's Filebrowser - Files - gray-sample.jpg");
await page.locator('button[aria-label="Previous"]').waitFor({ state: 'hidden' });
await page.locator('button[aria-label="Next"]').waitFor({ state: 'hidden' });