Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f9bb0789e | ||
|
|
054114fd97 | ||
|
|
9c7759ea20 | ||
|
|
cfaca1183e | ||
|
|
237994370b | ||
|
|
a8d66a4009 |
No files matched your search
+17
-13
@@ -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 }}
|
||||
@@ -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**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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' });
|
||||
|
||||
Reference in new issue
Block a user