Compare commits

...
4 Commits
Author SHA1 Message Date
Graham Steffaniak 38adbf6379 Clear temp dir (#506) 2025-04-08 09:28:57 -05:00
Graham Steffaniak 94e4d59e42 Clear temp dir (#504) 2025-04-07 20:02:41 -05:00
Graham Steffaniak 43167d69d3 updated with fix (#503) 2025-04-07 19:43:24 -05:00
Graham Steffaniak 266891fdab updated 2025-04-07 17:14:59 -05:00
31 changed files with 486 additions and 293 deletions

No files matched your search

+13
View File
@@ -2,6 +2,19 @@
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.7-beta
**Notes**:
- added full tests for single source example.
- adds descriptive error if temp dir can't be created on fatal startup
- clears temp directory on shutdown.
- removed put settings api (unused)
- removed more unused config properties.
**BugFixes**:
- fix url encoding issue for search links when theres only one source https://github.com/gtsteffaniak/filebrowser/issues/501
- files with # could have problems, double encoded.
## v0.6.6-beta
**New Feature**:
@@ -10,7 +10,7 @@ import { defineConfig, devices } from "@playwright/test";
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
//globalSetup: "./global-setup",
globalSetup: "./tests-playwright/noauth-setup",
timeout: 5000,
testDir: "./tests-playwright/noauth",
/* Run tests in files in parallel */
@@ -25,6 +25,7 @@ export default defineConfig({
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
storageState: "noauth.json",
actionTimeout: 5000,
//storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
+2
View File
@@ -8,6 +8,7 @@ server:
config:
defaultEnabled: true
createUserDir: true
frontend:
name: "Graham's Filebrowser"
disableDefaultLinks: true
@@ -15,6 +16,7 @@ frontend:
- text: "A playwright test"
url: "https://playwright.dev/"
title: "Playwright"
auth:
signup: false
methods:
+6 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/diskcache"
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/fileutils"
fbhttp "github.com/gtsteffaniak/filebrowser/backend/http"
"github.com/gtsteffaniak/filebrowser/backend/img"
"github.com/gtsteffaniak/filebrowser/backend/logger"
@@ -101,6 +102,7 @@ func StartFilebrowser() {
case <-done:
logger.Info("Server stopped unexpectedly. Shutting down...")
}
fileutils.ClearCacheDir()
<-shutdownComplete // Ensure we don't exit prematurely
// Wait for the server to stop
@@ -121,7 +123,10 @@ func rootCMD(ctx context.Context, store *storage.Storage, serverConfig *settings
var err error
fileCache, err = diskcache.NewFileCache(cacheDir)
if err != nil {
logger.Fatal(fmt.Sprintf("failed to create file cache: %v", err))
if cacheDir == "tmp" {
logger.Error("The cache dir could not be created. Make sure the user that you executed the program with has access to create directories in the local path. filebrowser is trying to use the default `server.cacheDir: tmp` , but you can change this location if you need to. Please see configuration wiki for more information about this error. https://github.com/gtsteffaniak/filebrowser/wiki/Configuration")
}
logger.Fatal(fmt.Sprintf("failed to create file cache path, which is now require to run the server: %v", err))
}
} else {
// No-op cache if no cacheDir is specified
+3
View File
@@ -71,6 +71,9 @@ func (idx *Index) Search(search string, scope string, sourceSession string) []Se
// search files first
for _, item := range dir.Files {
fullPath := dirName + "/" + item.Name
if dirName == "/" {
fullPath = dirName + item.Name
}
value, found := sessionInProgress.Load(sourceSession)
if !found || value != runningHash {
idx.mu.Unlock()
+23 -1
View File
@@ -90,7 +90,8 @@ func TestSearchWhileIndexing(t *testing.T) {
func TestSearchIndexes(t *testing.T) {
index := Index{
Directories: map[string]*FileInfo{
"/test": {Files: []ItemInfo{{Name: "audio1.wav", Type: "audio"}}},
"/": {Files: []ItemInfo{{Name: "audio-one.wav", Type: "audio"}}},
"/test": {Files: []ItemInfo{{Name: "audio-one.wav", Type: "audio"}}},
"/test/path": {Files: []ItemInfo{{Name: "file.txt", Type: "text"}}},
"/new/test": {Files: []ItemInfo{
{Name: "audio.wav", Type: "audio"},
@@ -230,6 +231,27 @@ func TestSearchIndexes(t *testing.T) {
},
},
},
{
search: "audio",
scope: "/",
expectedResult: []SearchResult{
{
Path: "/audio-one.wav",
Type: "audio",
Size: 0,
},
{
Path: "/test/audio-one.wav",
Type: "audio",
Size: 0,
},
{
Path: "/new/test/audio.wav",
Type: "audio",
Size: 0,
},
},
},
}
for _, tt := range tests {
+18
View File
@@ -8,6 +8,7 @@ import (
"path/filepath"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
)
// MoveFile moves a file from src to dst.
@@ -175,3 +176,20 @@ func CommonPrefix(sep byte, paths ...string) string {
return string(c)
}
func ClearCacheDir() {
cacheDir := settings.Config.Server.CacheDir
entries, err := os.ReadDir(cacheDir)
if err != nil {
logger.Error(fmt.Sprintf("failed clear cache dir: %v", err))
}
for _, entry := range entries {
path := filepath.Join(cacheDir, entry.Name())
err = os.RemoveAll(path)
if err != nil {
logger.Error(fmt.Sprintf("failed clear cache dir: %v", err))
}
}
}
-1
View File
@@ -114,7 +114,6 @@ func StartHttp(ctx context.Context, Service ImgService, storage *storage.Storage
// Settings routes
api.HandleFunc("GET /settings", withAdmin(settingsGetHandler))
api.HandleFunc("PUT /settings", withAdmin(settingsPutHandler))
api.HandleFunc("GET /onlyoffice/config", withUser(onlyofficeClientConfigGetHandler))
api.HandleFunc("POST /onlyoffice/callback", withUser(onlyofficeCallbackHandler))
-37
View File
@@ -1,20 +1,9 @@
package http
import (
"encoding/json"
"net/http"
"github.com/gtsteffaniak/filebrowser/backend/settings"
)
type settingsData struct {
Signup bool `json:"signup"`
CreateUserDir bool `json:"createUserDir"`
UserHomeBasePath string `json:"userHomeBasePath"`
Defaults settings.UserDefaults `json:"defaults"`
Frontend settings.Frontend `json:"frontend"`
}
// settingsGetHandler retrieves the current system settings.
// @Summary Get system settings
// @Description Returns the current configuration settings for signup, user directories, rules, frontend.
@@ -45,29 +34,3 @@ func settingsGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
}
return renderJSON(w, r, config)
}
// settingsPutHandler updates the system settings.
// @Summary Update system settings
// @Description Updates the system configuration settings for signup, user directories, rules, frontend.
// @Tags Settings
// @Accept json
// @Produce json
// @Param body body settingsData true "Settings data to update"
// @Success 200 "Settings updated successfully"
// @Failure 400 {object} map[string]string "Bad request - failed to decode body"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/settings [put]
func settingsPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
req := &settingsData{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
return http.StatusBadRequest, err
}
config.Server.UserHomeBasePath = req.UserHomeBasePath
config.UserDefaults = req.Defaults
config.Frontend = req.Frontend
config.Auth.Signup = req.Signup
err = store.Settings.Save(config)
return errToStatus(err), err
}
-17
View File
@@ -5,13 +5,10 @@ import (
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/logger"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/version"
)
@@ -47,7 +44,6 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
"DisableExternal": config.Frontend.DisableDefaultLinks,
"DisableUsedPercentage": config.Frontend.DisableUsedPercentage,
"darkMode": settings.Config.UserDefaults.DarkMode,
"Color": config.Frontend.Color,
"BaseURL": config.Server.BaseURL,
"Version": version.Version,
"CommitSHA": version.CommitSHA,
@@ -68,19 +64,6 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
"SourceCount": len(config.Server.Sources),
}
if config.Frontend.Files != "" {
fPath := filepath.Join(config.Frontend.Files, "custom.css")
_, err := os.Stat(fPath) //nolint:govet
if err != nil && !os.IsNotExist(err) {
logger.Error(fmt.Sprintf("couldn't load custom styles: %v", err))
}
if err == nil {
data["CSS"] = true
}
}
if config.Auth.Methods.PasswordAuth.Enabled {
raw, err := store.Auth.Get("password") //nolint:govet
if err != nil {
-3
View File
@@ -28,9 +28,6 @@ func (s *Storage) Get() (*Settings, error) {
if err != nil {
return nil, err
}
if set.Server.UserHomeBasePath == "" {
set.Server.UserHomeBasePath = DefaultUsersHomeBasePath
}
return set, nil
}
+9 -17
View File
@@ -13,24 +13,20 @@ const (
)
type Settings struct {
Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"`
Server Server `json:"server"`
Auth Auth `json:"auth"`
Frontend Frontend `json:"frontend"`
Users []UserDefaults `json:"users,omitempty"`
UserDefaults UserDefaults `json:"userDefaults"`
Integrations Integrations `json:"integrations"`
Server Server `json:"server"`
Auth Auth `json:"auth"`
Frontend Frontend `json:"frontend"`
Users []UserDefaults `json:"users,omitempty"`
UserDefaults UserDefaults `json:"userDefaults"`
Integrations Integrations `json:"integrations"`
}
type Auth struct {
TokenExpirationHours int `json:"tokenExpirationHours"`
Recaptcha Recaptcha `json:"recaptcha"`
Methods LoginMethods `json:"methods"`
Command string `json:"command"`
Signup bool `json:"signup"`
Method string `json:"method"`
Shell string `json:"shell"`
Key []byte `json:"key"`
AdminUsername string `json:"adminUsername"`
AdminPassword string `json:"adminPassword"`
@@ -73,17 +69,15 @@ type Server struct {
BaseURL string `json:"baseURL"`
Logging []LogConfig `json:"logging"`
Database string `json:"database"`
Root string `json:"root"` // deprecated, use sources
UserHomeBasePath string `json:"userHomeBasePath"`
Sources []Source `json:"sources"`
ExternalUrl string `json:"externalUrl"`
InternalUrl string `json:"internalUrl"` // used by integrations
CacheDir string `json:"cacheDir"`
MaxArchiveSizeGB int64 `json:"maxArchiveSize"`
// not exposed to config
SourceMap map[string]Source // uses realpath as key
NameToSource map[string]Source // uses name as key
DefaultSource Source
SourceMap map[string]Source `json:"-"` // uses realpath as key
NameToSource map[string]Source `json:"-"` // uses name as key
DefaultSource Source `json:"-"`
}
type Integrations struct {
@@ -135,8 +129,6 @@ type Frontend struct {
Name string `json:"name"`
DisableDefaultLinks bool `json:"disableDefaultLinks"`
DisableUsedPercentage bool `json:"disableUsedPercentage"`
Files string `json:"files"`
Color string `json:"color"`
ExternalLinks []ExternalLink `json:"externalLinks"`
}
-53
View File
@@ -917,53 +917,6 @@ const docTemplate = `{
}
}
}
},
"put": {
"description": "Updates the system configuration settings for signup, user directories, rules, frontend.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Settings"
],
"summary": "Update system settings",
"parameters": [
{
"description": "Settings data to update",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/http.settingsData"
}
}
],
"responses": {
"200": {
"description": "Settings updated successfully"
},
"400": {
"description": "Bad request - failed to decode body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/share": {
@@ -1784,9 +1737,6 @@ const docTemplate = `{
"settings.Frontend": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"disableDefaultLinks": {
"type": "boolean"
},
@@ -1799,9 +1749,6 @@ const docTemplate = `{
"$ref": "#/definitions/settings.ExternalLink"
}
},
"files": {
"type": "string"
},
"name": {
"type": "string"
}
-53
View File
@@ -906,53 +906,6 @@
}
}
}
},
"put": {
"description": "Updates the system configuration settings for signup, user directories, rules, frontend.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Settings"
],
"summary": "Update system settings",
"parameters": [
{
"description": "Settings data to update",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/http.settingsData"
}
}
],
"responses": {
"200": {
"description": "Settings updated successfully"
},
"400": {
"description": "Bad request - failed to decode body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/share": {
@@ -1773,9 +1726,6 @@
"settings.Frontend": {
"type": "object",
"properties": {
"color": {
"type": "string"
},
"disableDefaultLinks": {
"type": "boolean"
},
@@ -1788,9 +1738,6 @@
"$ref": "#/definitions/settings.ExternalLink"
}
},
"files": {
"type": "string"
},
"name": {
"type": "string"
}
-36
View File
@@ -117,8 +117,6 @@ definitions:
type: object
settings.Frontend:
properties:
color:
type: string
disableDefaultLinks:
type: boolean
disableUsedPercentage:
@@ -127,8 +125,6 @@ definitions:
items:
$ref: '#/definitions/settings.ExternalLink'
type: array
files:
type: string
name:
type: string
type: object
@@ -900,38 +896,6 @@ paths:
summary: Get system settings
tags:
- Settings
put:
consumes:
- application/json
description: Updates the system configuration settings for signup, user directories,
rules, frontend.
parameters:
- description: Settings data to update
in: body
name: body
required: true
schema:
$ref: '#/definitions/http.settingsData'
produces:
- application/json
responses:
"200":
description: Settings updated successfully
"400":
description: Bad request - failed to decode body
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
summary: Update system settings
tags:
- Settings
/api/share:
get:
consumes:
+1 -1
View File
@@ -211,7 +211,7 @@ export function getDownloadURL(source, path, inline, useExternal) {
const apiPath = getApiPath('api/raw', params)
if (externalUrl && useExternal) {
return externalUrl + removePrefix(apiPath,baseURL)
return externalUrl + removePrefix(apiPath, baseURL)
}
return window.origin + apiPath
} catch (err) {
+12 -3
View File
@@ -403,8 +403,17 @@ export default {
this.submit();
},
getRelative(path) {
// double encode # to fix issue with # in path
// replace all # with %23
path = path.replace(/#/g, "%23");
path = path.slice(1); // remove leading slash
const fullpath = "/files/" + this.selectedSource + "/" + encodeURIComponent(path);
let fullpath = path;
if (state.sources.count === 1) {
fullpath = "/files/" + encodeURIComponent(path);
} else {
fullpath = "/files/" + this.selectedSource + "/" + encodeURIComponent(path);
}
return fullpath;
},
getIcon(mimetype) {
@@ -430,9 +439,9 @@ export default {
return "";
}
parts.pop();
parts = parts.join("/") + "/";
parts = parts.join("/");
if (isDir) {
parts = "/" + parts; // fix weird rtl thing
parts = "/" + parts + "/"; // fix weird rtl thing
}
return parts;
@@ -1,30 +0,0 @@
<template>
<div>
<h3>{{ $t("settings.userCommands") }}</h3>
<p class="small">
{{ $t("settings.userCommandsHelp") }} <i>git svn hg</i>.
</p>
<input class="input input--block" type="text" v-model.trim="raw" />
</div>
</template>
<script>
export default {
name: "permissions",
props: ["commands"],
computed: {
raw: {
get() {
return this.commands.join(" ");
},
set(value) {
if (value !== "") {
this.$emit("update:commands", value.split(" "));
} else {
this.$emit("update:commands", []);
}
},
},
},
};
</script>
@@ -22,7 +22,6 @@
</template>
<script>
import { enableExec } from "@/utils/constants";
export default {
name: "permissions",
props: ["perm"],
@@ -41,7 +40,6 @@ export default {
this.perm.admin = value;
},
},
isExecEnabled: () => enableExec,
},
};
</script>
@@ -101,15 +101,12 @@
</p>
<permissions :perm="localUser.perm" />
<commands v-if="isExecEnabled" v-model:commands="user.commands" />
</div>
</template>
<script>
import Languages from "./Languages.vue";
import Permissions from "./Permissions.vue";
import Commands from "./Commands.vue";
import { enableExec } from "@/utils/constants";
import { state } from "@/store";
import { settingsApi } from "@/api";
@@ -118,7 +115,6 @@ export default {
components: {
Permissions,
Languages,
Commands,
},
data() {
return {
@@ -193,9 +189,6 @@ export default {
displayHomeDirectoryCheckbox() {
return this.isNew && this.createUserDir;
},
isExecEnabled() {
return enableExec; // Removed arrow function
},
},
methods: {
setUpdatePassword() {
-2
View File
@@ -15,7 +15,6 @@ const noAuth = window.FileBrowser.NoAuth;
const loginPage = window.FileBrowser.LoginPage;
const enableThumbs = window.FileBrowser.EnableThumbs;
const resizePreview = window.FileBrowser.ResizePreview;
const enableExec = window.FileBrowser.EnableExec;
const externalUrl = window.FileBrowser.ExternalUrl
const onlyOfficeUrl = window.FileBrowser.OnlyOfficeUrl
const serverHasMultipleSources = window.FileBrowser.SourceCount > 1;
@@ -47,7 +46,6 @@ export {
loginPage,
enableThumbs,
resizePreview,
enableExec,
origin,
darkMode,
settings,
-1
View File
@@ -66,7 +66,6 @@ export function removePrefix(path, prefix = "") {
if (!path.startsWith('/')) {
path = '/' + path;
}
return path;
}
-4
View File
@@ -42,7 +42,6 @@ import Sidebar from "@/components/sidebar/Sidebar.vue";
import ContextMenu from "@/components/ContextMenu.vue";
import Notifications from "@/components/Notifications.vue";
import { enableExec } from "@/utils/constants";
import { state, getters, mutations } from "@/store";
export default {
@@ -107,9 +106,6 @@ export default {
isDarkMode() {
return getters.isDarkMode();
},
isExecEnabled() {
return enableExec;
},
currentView() {
return getters.currentView();
},
+2 -3
View File
@@ -6,7 +6,8 @@
</div>
<div class="card-content">
Currently there are no global config options, You can edit the configuration yaml to adjust system settings.
Currently there are no global config options, You can edit the configuration yaml to
adjust system settings.
</div>
<div class="card-action">
@@ -19,7 +20,6 @@
import { notify } from "@/notify";
import { state, mutations, getters } from "@/store";
import { settingsApi } from "@/api";
import { enableExec } from "@/utils/constants";
import Errors from "@/views/Errors.vue";
export default {
@@ -41,7 +41,6 @@ export default {
user() {
return state.user;
},
isExecEnabled: () => enableExec,
},
async created() {
mutations.setLoading("settings", true);
+3 -3
View File
@@ -10,13 +10,13 @@ async function globalSetup() {
await page.getByPlaceholder("Username").fill("admin");
await page.getByPlaceholder("Password").fill("admin");
await page.getByRole("button", { name: "Login" }).click();
await page.waitForURL("**/files/", { timeout: 250 });
await page.waitForURL("**/files/", { timeout: 500 });
const cookies = await context.cookies();
expect(cookies.find((c) => c.name === "auth")?.value).toBeDefined();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.waitForURL("**/files/playwright-files", { timeout: 250 });
await page.waitForURL("**/files/playwright-files", { timeout: 500 });
// Create a share of folder
await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' });
@@ -36,7 +36,7 @@ async function globalSetup() {
localStorage.setItem('shareHash', hash);
}, shareHash);
await page.goto("http://127.0.0.1/files/playwright-files", { timeout: 250 });
await page.goto("http://127.0.0.1/files/playwright-files", { timeout: 500 });
// Create a share of file
await page.locator('a[aria-label="1file1.txt"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="1file1.txt"]').click({ button: "right" });
+53
View File
@@ -0,0 +1,53 @@
import { Browser, firefox, expect, Page } from "@playwright/test";
// Perform authentication and store auth state
async function globalSetup() {
const browser: Browser = await firefox.launch();
const context = await browser.newContext();
const page: Page = await context.newPage();
await page.goto("http://127.0.0.1/files", { timeout: 500 });
// Create a share of folder
await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="myfolder"]').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="Share"]').click();
await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /myfolder');
await page.locator('button[aria-label="Share-Confirm"]').click();
await expect(page.locator("#share .card-content table tbody tr:not(:has(th))")).toHaveCount(1);
const shareHash = await page.locator("#share .card-content table tbody tr:not(:has(th)) td").first().textContent();
if (!shareHash) {
throw new Error("Failed to retrieve shareHash");
}
// Store shareHash in localStorage
await page.evaluate((hash) => {
localStorage.setItem('shareHash', hash);
}, shareHash);
await page.goto("http://127.0.0.1/files", { timeout: 500 });
// Create a share of file
await page.locator('a[aria-label="1file1.txt"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="1file1.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="Share"]').click();
await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /1file1.txt');
await page.locator('button[aria-label="Share-Confirm"]').click();
await expect(page.locator("#share .card-content table tbody tr:not(:has(th))")).toHaveCount(1);
const shareHashFile = await page.locator("#share .card-content table tbody tr:not(:has(th)) td").first().textContent();
if (!shareHashFile) {
throw new Error("Failed to retrieve shareHash");
}
// Store shareHash in localStorage
await page.evaluate((hash) => {
localStorage.setItem('shareHashFile', hash);
}, shareHashFile);
await context.storageState({ path: "./noauth.json" });
await browser.close();
}
export default globalSetup;
@@ -0,0 +1,120 @@
import { test, expect } from "../test-setup";
test("info from listing", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file.tar.gz"]').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="Info"]').click();
await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz');
checkForErrors();
});
test("info from search", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('#search').click()
await page.locator('#main-input').fill('file.tar.gz');
await expect(page.locator('#result-list')).toHaveCount(1);
await page.locator('li[aria-label="file.tar.gz"]').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="Info"]').click();
await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz');
checkForErrors();
})
test("open from search", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('#search').click()
await page.locator('#main-input').fill('file.tar.gz');
await expect(page.locator('#result-list')).toHaveCount(1);
await page.locator('li[aria-label="file.tar.gz"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file.tar.gz");
await expect(page.locator('#previewer')).toContainText('Preview is not available for this file.');
checkForErrors();
})
test("2x copy from listing to new folder", async({ page, checkForErrors, 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' });
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: /');
await expect(page.locator('li[aria-selected="true"]')).toHaveCount(0);
await page.locator('li[aria-label="myfolder"]').click();
await expect(page.locator('li[aria-selected="true"]')).toHaveCount(1);
await page.locator('button[aria-label="Copy"]').click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Successfully copied file/folder, redirecting...");
await page.waitForURL('**/myfolder/');
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="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('**/newfolder/');
await expect(page).toHaveTitle("Graham's Filebrowser - Files - newfolder");
checkForErrors();
})
test("delete file", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="deleteme.txt"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="deleteme.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="Delete"]').click();
await expect( page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/deleteme.txt');
await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/deleteme.txt');
await page.locator('button[aria-label="Confirm-Delete"]').click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText("Deleted item successfully! reloading...");
checkForErrors();
})
test("delete nested file prompt", async({ page, checkForErrors, context }) => {
await page.goto("/files/files/folder%23hash/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - folder#hash");
await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file#.sh"]').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="Delete"]').click();
await expect(page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/folder#hash/file#.sh');
await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/folder#hash/file#.sh');
checkForErrors();
})
@@ -0,0 +1,60 @@
import { test, expect } from "../test-setup";
test("navigate with hash in file name", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('a[aria-label="folder#hash"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="folder#hash"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - folder#hash");
await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' });
await page.locator('a[aria-label="file#.sh"]').dblclick();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - file#.sh");
await expect(page.locator('.topTitle')).toHaveText('file#.sh');
checkForErrors()
})
test("breadcrumbs navigation checks", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page.locator('a[aria-label="Home"]')).toHaveAttribute("href", "/files/");
// Ensure no <span> children exist directly under .breadcrumbs (ie no breadcrumbs paths)
let spanChildrenCount = await page.locator('.breadcrumbs > span').count();
await page.waitForSelector('.breadcrumbs');
expect(spanChildrenCount).toBe(0);
await page.goto("/files/myfolder");
await page.waitForSelector('.breadcrumbs');
spanChildrenCount = await page.locator('.breadcrumbs > span').count();
expect(spanChildrenCount).toBe(1);
let breadCrumbLink = page.locator('span[aria-label="breadcrumb-link-myfolder"] a')
await expect(breadCrumbLink).toHaveText("myfolder");
await page.goto("/files/myfolder/testdata");
await page.waitForSelector('.breadcrumbs');
spanChildrenCount = await page.locator('.breadcrumbs > span').count();
expect(spanChildrenCount).toBe(2);
breadCrumbLink = page.locator('span[aria-label="breadcrumb-link-testdata"] a')
await expect(breadCrumbLink).toHaveText("testdata");
// TODO: fix this test.. router issue for /files path
//await page.goto("/files/files");
//await page.waitForSelector('.breadcrumbs');
//spanChildrenCount = await page.locator('.breadcrumbs > span').count();
//expect(spanChildrenCount).toBe(1);
//breadCrumbLink = page.locator('span[aria-label="breadcrumb-link-files"] a')
//await expect(breadCrumbLink).toHaveText("files");
checkForErrors();
});
test("navigate from search item", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('#search').click()
await page.locator('#main-input').fill('for testing');
await expect(page.locator('#result-list')).toHaveCount(1);
await page.locator('li[aria-label="for testing.md"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Files - for testing.md");
await expect(page.locator('.topTitle')).toHaveText('for testing.md');
checkForErrors()
});
@@ -0,0 +1,102 @@
import { test, expect } from "../test-setup";
test("breadcrumbs navigation checks", async ({ page, checkForErrors, context }) => {
await page.goto("/files/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("/files/share/" + shareHash);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - myfolder");
await expect(page.locator('a[aria-label="Home"]')).toHaveAttribute("href", `/files/share/${shareHash}/`);
await page.dblclick('a[aria-label="testdata"]');
await expect(page).toHaveTitle("Graham's Filebrowser - Share - testdata");
// Ensure no <span> children exist directly under .breadcrumbs (ie no breadcrumbs paths)
let spanChildrenCount = await page.locator('.breadcrumbs > span').count();
await page.waitForSelector('.breadcrumbs');
expect(spanChildrenCount).toBe(1);
checkForErrors();
});
test("root share path is valid", async ({ page, checkForErrors, context }) => {
await page.goto("/files/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('button[aria-label="Show-Share"]').waitFor({ state: 'visible' });
await page.locator('button[aria-label="Show-Share"]').click();
await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /');
checkForErrors();
});
test("share file works", async ({ page, checkForErrors, context }) => {
await page.goto("/files/files/");
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
const shareHashFile = await page.evaluate(() => localStorage.getItem('shareHashFile'));
if (shareHashFile == "") {
throw new Error("Share hash not found in localStorage");
}
await page.goto("/files/share/" + shareHashFile);
await expect(page).toHaveTitle("Graham's Filebrowser - Share - 1file1.txt");
await expect(page.locator('a[aria-label="Home"]')).toHaveAttribute("href", `/files/share/${shareHashFile}/`);
checkForErrors();
});
test("share download single file", async ({ page, checkForErrors, context }) => {
await page.goto("/files/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("/files/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/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("/files/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();
});
@@ -0,0 +1,45 @@
import { test, expect } from "../test-setup";
test("sidebar links", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
// Verify the page title
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
// Locate the credits container
const credits = page.locator('.credits'); // Fix the selector to match the HTML structure
// Assert that the <h3> contains the text 'FileBrowser Quantum'
await expect(credits.locator("h4")).toHaveText("Graham's Filebrowser");
// Assert that the <a> contains the text 'A playwright test'
await expect(credits.locator("span").locator("a")).toHaveText('A playwright test');
// Assert that the <a> does not contain the text 'Help'
await expect(credits.locator("span").locator("a")).not.toHaveText('Help');
// Check for console errors
checkForErrors();
});
test("adjusting theme colors", async({ page, checkForErrors, context }) => {
await page.goto("/files/");
const originalPrimaryColor = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue('--primaryColor').trim();
});
await expect(originalPrimaryColor).toBe('#2196f3');
// Verify the page title
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
await page.locator('i[aria-label="settings"]').click();
await expect(page).toHaveTitle("Graham's Filebrowser - Settings");
await page.locator('button', { hasText: 'violet' }).click();
const popup = page.locator('#popup-notification-content');
await popup.waitFor({ state: 'visible' });
await expect(popup).toHaveText('Settings updated!');
const newPrimaryColor = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue('--primaryColor').trim();
});
await expect(newPrimaryColor).toBe('#9b59b6');
// Check for console errors
checkForErrors();
});
+12 -17
View File
@@ -1,13 +1,13 @@
SHELL := /bin/bash
.SILENT:
setup:
echo "creating ./backend/test_config.yaml for local testing..." && \
echo "creating ./backend/test_config.yaml for local testing..."
if [ ! -f backend/test_config.yaml ]; then \
cp backend/config.yaml backend/test_config.yaml; \
fi
echo "installing swagger needed to generate backend api docs..." && \
go install github.com/swaggo/swag/cmd/swag@latest && \
echo "installing npm requirements for frontend..." && \
echo "installing swagger needed to generate backend api docs..."
go install github.com/swaggo/swag/cmd/swag@latest
echo "installing npm requirements for frontend..."
cd frontend && npm i
update:
@@ -30,8 +30,8 @@ run: build-frontend
--ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'" . -c test_config.yaml
build-frontend:
cd backend/http && rm -rf dist && rm -rf embed/* && ln -s ../../frontend/dist && \
cd ../../frontend && npm run build
cd backend/http && rm -rf dist && rm -rf embed/* && ln -s ../../frontend/dist
cd frontend && npm run build
lint-frontend:
cd frontend && npm run lint
@@ -52,15 +52,10 @@ test-frontend:
cd frontend && npm run test
test-playwright: build-frontend
cd backend && GOOS=linux go build -o filebrowser . && cd .. && \
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-general . && \
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests && \
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-noauth . && \
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests && \
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-proxy . && \
cd backend && GOOS=linux go build -o filebrowser .
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-general .
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests
# Run on a windows machine!
release-windows:
cd frontend && npm run build-windows && \
cd ../backend && go build --ldflags="-w -s -X github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version='" .
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-noauth .
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests
docker build -t filebrowser-playwright-tests -f _docker/Dockerfile.playwright-proxy .
docker run --rm --name filebrowser-playwright-tests filebrowser-playwright-tests