chore: merge upstream v2.26.0 (#44)

pull/3756/head
Laurynas Gadliauskas 2023-12-22 14:50:11 +02:00 committed by GitHub
parent 8fe60bc816
commit 67e9270877
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 2957 additions and 8903 deletions

View File

@ -1,4 +1,5 @@
* *
!docker/* !docker/*
!healthcheck.sh
!docker_config.json !docker_config.json
!filebrowser !filebrowser

View File

@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '16' node-version: '18'
- run: make lint-frontend - run: make lint-frontend
lint-backend: lint-backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: 1.20.6 go-version: 1.21.0
- run: make lint-backend - run: make lint-backend
lint-commits: lint-commits:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -34,7 +34,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '16' node-version: '18'
- run: make lint-commits - run: make lint-commits
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -49,7 +49,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '16' node-version: '18'
- run: make test-frontend - run: make test-frontend
test-backend: test-backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: 1.20.6 go-version: 1.21.0
- run: make test-backend - run: make test-backend
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest

3
.gitignore vendored
View File

@ -31,6 +31,9 @@ bin/
dist/ dist/
build/ build/
/frontend/dist/*
!/frontend/dist/.gitkeep
# Hostinger-specific files # Hostinger-specific files
.cagefs/ .cagefs/
.filebrowser/ .filebrowser/

View File

@ -53,7 +53,7 @@ linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- depguard - deadcode
- dogsled - dogsled
- dupl - dupl
- errcheck - errcheck

View File

@ -3,32 +3,33 @@ project_name: filebrowser
env: env:
- GO111MODULE=on - GO111MODULE=on
build: builds:
env: - env:
- CGO_ENABLED=0 - CGO_ENABLED=0
ldflags: ldflags:
- -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }} - -s -w -X github.com/filebrowser/filebrowser/v2/version.Version={{ .Version }} -X github.com/filebrowser/filebrowser/v2/version.CommitSHA={{ .ShortCommit }}
main: main.go main: main.go
binary: filebrowser binary: filebrowser
goos: goos:
- darwin - darwin
- linux - linux
- windows - windows
- freebsd - freebsd
goarch: goarch:
- amd64 - amd64
- 386 - 386
- arm - arm
- arm64 - arm64
goarm: - riscv64
- 5 goarm:
- 6 - 5
- 7 - 6
ignore: - 7
- goos: darwin ignore:
goarch: 386 - goos: darwin
- goos: freebsd goarch: 386
goarch: arm - goos: freebsd
goarch: arm
archives: archives:
- -
@ -185,7 +186,7 @@ docker_manifests:
- "filebrowser/filebrowser:v{{ .Major }}-arm64-s6" - "filebrowser/filebrowser:v{{ .Major }}-arm64-s6"
brews: brews:
- name: filebrowser - name: filebrowser
tap: repository:
owner: filebrowser owner: filebrowser
name: homebrew-tap name: homebrew-tap
folder: Formula folder: Formula

View File

@ -2,6 +2,78 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.26.0](https://github.com/filebrowser/filebrowser/compare/v2.25.0...v2.26.0) (2023-11-02)
### Features
* add modern greek translation ([#2778](https://github.com/filebrowser/filebrowser/issues/2778)) ([c3079d3](https://github.com/filebrowser/filebrowser/commit/c3079d30e22385d7e677f172324cd9cbab6487ce))
* make user session timeout configurable ([#2753](https://github.com/filebrowser/filebrowser/issues/2753)) ([7fabadc](https://github.com/filebrowser/filebrowser/commit/7fabadc871ea91ea22fe9454e2ca4b33e5c211be))
### Bug Fixes
* avoid the front-end calling api/renew loop ([#2792](https://github.com/filebrowser/filebrowser/issues/2792)) ([edd808f](https://github.com/filebrowser/filebrowser/commit/edd808f124f4ada99bcbe4bca98ddbe20e5a424c))
* disable static resource files listing ([da1fe7c](https://github.com/filebrowser/filebrowser/commit/da1fe7c9d76a9c6a25bfa19ebd6cf8023eff5d62))
* display file size as base 2 (KiB instead of KB) ([#2779](https://github.com/filebrowser/filebrowser/issues/2779)) ([cdcd9a3](https://github.com/filebrowser/filebrowser/commit/cdcd9a313aa50c2e6806a182b6838462d42dcafe))
* goreleaser yaml ([4d0a68e](https://github.com/filebrowser/filebrowser/commit/4d0a68e7875274f4c939f2bfa15739a9b0ecf70a))
* revert fetchURL changes in auth (Fixes [#2729](https://github.com/filebrowser/filebrowser/issues/2729)) ([#2739](https://github.com/filebrowser/filebrowser/issues/2739)) ([bd3c194](https://github.com/filebrowser/filebrowser/commit/bd3c1941ff8289a5dae877e08f7e25fa9b2a92c5))
* solve docker build failed issue ([#2797](https://github.com/filebrowser/filebrowser/issues/2797)) ([6a31af6](https://github.com/filebrowser/filebrowser/commit/6a31af6c0a144128af865d802c8039fa5250e946))
### Build
* **deps-dev:** bump postcss from 8.4.27 to 8.4.31 in /frontend ([#2749](https://github.com/filebrowser/filebrowser/issues/2749)) ([21d361a](https://github.com/filebrowser/filebrowser/commit/21d361ad308d109d2a6b323597019aaa09ce1781))
* **deps:** bump @babel/traverse in /frontend ([#2775](https://github.com/filebrowser/filebrowser/issues/2775)) ([bb4bb50](https://github.com/filebrowser/filebrowser/commit/bb4bb508a9d71516e8fa80b3a6285fe002a059d2))
* **deps:** bump golang.org/x/image from 0.5.0 to 0.10.0 ([#2800](https://github.com/filebrowser/filebrowser/issues/2800)) ([a744bd2](https://github.com/filebrowser/filebrowser/commit/a744bd224f0ff1efc53ab94481fa76ef68788df1))
* **deps:** bump golang.org/x/net from 0.11.0 to 0.17.0 ([#2758](https://github.com/filebrowser/filebrowser/issues/2758)) ([d574fb6](https://github.com/filebrowser/filebrowser/commit/d574fb6d1af41ec31778b0f402674e5111a7875d))
* fix deprecated goreleaser config options ([38f7788](https://github.com/filebrowser/filebrowser/commit/38f77882559133b9ff330cfb955a9d4ea4728cf8))
## [2.25.0](https://github.com/filebrowser/filebrowser/compare/v2.24.2...v2.25.0) (2023-09-14)
### Features
* add new folder button to move/create dialogs ([#2667](https://github.com/filebrowser/filebrowser/issues/2667)) ([5994224](https://github.com/filebrowser/filebrowser/commit/599422446849fa37d5ab448bbf464afb7304b99d))
* added shell resizing ([#2648](https://github.com/filebrowser/filebrowser/issues/2648)) ([584b706](https://github.com/filebrowser/filebrowser/commit/584b706b1e310297acc2580c60442ff5c11ae432))
* implement abort upload functionality ([#2673](https://github.com/filebrowser/filebrowser/issues/2673)) ([a404fb0](https://github.com/filebrowser/filebrowser/commit/a404fb043da2573bf04385863b2d34b1f918b8e1))
* implement upload speed calculation and ETA estimation ([#2677](https://github.com/filebrowser/filebrowser/issues/2677)) ([ecdd684](https://github.com/filebrowser/filebrowser/commit/ecdd684bf1d537a4591caa38348102b61dd51e5d))
### Bug Fixes
* refactor path resolution logic for project root ([#2674](https://github.com/filebrowser/filebrowser/issues/2674)) ([95fec7f](https://github.com/filebrowser/filebrowser/commit/95fec7f69430c108e5cf95c428db9d671cd97a94))
* tus upload with cloudflare proxy ([36af01d](https://github.com/filebrowser/filebrowser/commit/36af01daa6e04005ce3d18985eebaeef06f7393d)), closes [#2593](https://github.com/filebrowser/filebrowser/issues/2593)
### Refactorings
* migrate frontend tooling to vite 4 ([#2645](https://github.com/filebrowser/filebrowser/issues/2645)) ([8838a09](https://github.com/filebrowser/filebrowser/commit/8838a09cf5104deac22b6143050588040c6825e6))
### Build
* bump go version to 1.21.0 ([#2672](https://github.com/filebrowser/filebrowser/issues/2672)) ([2c97573](https://github.com/filebrowser/filebrowser/commit/2c97573301a1b13179678fb7f9bd8316539ecdff))
* bump node version to 18 ([#2671](https://github.com/filebrowser/filebrowser/issues/2671)) ([70eba7e](https://github.com/filebrowser/filebrowser/commit/70eba7ecc9d19545c0899ae40eb3897a7c48562f))
### Performance improvements
* **backend:** optimize subtitles detection performance ([#2637](https://github.com/filebrowser/filebrowser/issues/2637)) ([374bbd3](https://github.com/filebrowser/filebrowser/commit/374bbd3ec199fddbe491ab2b74e520a10a73e54b))
### [2.24.2](https://github.com/filebrowser/filebrowser/compare/v2.24.1...v2.24.2) (2023-08-08)
### Bug Fixes
* 403 error error when uploading ([#2598](https://github.com/filebrowser/filebrowser/issues/2598)) ([289c8e6](https://github.com/filebrowser/filebrowser/commit/289c8e6f32eb520cc711389f6b6a4ed94a73ecd4))
* config init for branding.disableUsedPercentage ([#2576](https://github.com/filebrowser/filebrowser/issues/2576)) ([#2596](https://github.com/filebrowser/filebrowser/issues/2596)) ([ff1e0b8](https://github.com/filebrowser/filebrowser/commit/ff1e0b8185faf14b1f8e91830ca5e71e68ab672e))
### Build
* add riscv64 binary releases ([#2587](https://github.com/filebrowser/filebrowser/issues/2587)) ([0ac3968](https://github.com/filebrowser/filebrowser/commit/0ac39684f175487314e97403406f4d2c482e3d79))
### [2.24.1](https://github.com/filebrowser/filebrowser/compare/v2.24.0...v2.24.1) (2023-07-31) ### [2.24.1](https://github.com/filebrowser/filebrowser/compare/v2.24.0...v2.24.1) (2023-07-31)

View File

@ -12,6 +12,12 @@
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app. filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app.
## Demo
url: https://demo.filebrowser.org/
credentials: `demo`/`demo`
## Features ## Features
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features) Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)

View File

@ -38,7 +38,7 @@ override the options.`,
Branding: settings.Branding{ Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"), Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"), DisableExternal: mustGetBool(flags, "branding.disableExternal"),
DisableUsedPercentage: true, DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
Files: mustGetString(flags, "branding.files"), Files: mustGetString(flags, "branding.files"),
}, },
} }

View File

@ -60,7 +60,7 @@ you want to change. Other options will remain unchanged.`,
case "branding.disableExternal": case "branding.disableExternal":
set.Branding.DisableExternal = mustGetBool(flags, flag.Name) set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
case "branding.disableUsedPercentage": case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage = true set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
case "branding.files": case "branding.files":
set.Branding.Files = mustGetString(flags, flag.Name) set.Branding.Files = mustGetString(flags, flag.Name)
} }

View File

@ -64,6 +64,7 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
flags.StringP("baseurl", "b", "", "base url") flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)") flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
flags.Bool("disable-thumbnails", false, "disable image thumbnails") flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews") flags.Bool("disable-preview-resize", false, "disable resize of image previews")
@ -262,6 +263,10 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
_, disableExec := getParamB(flags, "disable-exec") _, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec server.EnableExec = !disableExec
if val, set := getParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
if val, set := getParamB(flags, "hidden-files"); set { if val, set := getParamB(flags, "hidden-files"); set {
server.HiddenFiles = convertFileStrToFileMap(val) server.HiddenFiles = convertFileStrToFileMap(val)
} }

View File

@ -7,6 +7,7 @@ import (
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"hash" "hash"
"image"
"io" "io"
"log" "log"
"mime" "mime"
@ -29,23 +30,25 @@ const PermDir = 0755
// FileInfo describes a file. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
Fs afero.Fs `json:"-"` Fs afero.Fs `json:"-"`
Path string `json:"path"` Path string `json:"path"`
Name string `json:"name"` Name string `json:"name"`
Size int64 `json:"size"` Size int64 `json:"size"`
Extension string `json:"extension"` Extension string `json:"extension"`
ModTime time.Time `json:"modified"` ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"` Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"` IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"` IsSymlink bool `json:"isSymlink"`
Link string `json:"link"` Link string `json:"link"`
Type string `json:"type"` Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"` Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"` Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
DiskUsage int64 `json:"diskUsage,omitempty"` DiskUsage int64 `json:"diskUsage,omitempty"`
Inodes int64 `json:"inodes,omitempty"` Inodes int64 `json:"inodes,omitempty"`
currentDir []os.FileInfo `json:"-"`
Resolution *ImageResolution `json:"resolution,omitempty"`
} }
// FileOptions are the options when getting a file info. // FileOptions are the options when getting a file info.
@ -60,6 +63,11 @@ type FileOptions struct {
Content bool Content bool
} }
type ImageResolution struct {
Width int `json:"width"`
Height int `json:"height"`
}
// NewFileInfo creates a File object from a path and a given user. This File // NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory // object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles. // or a file. If it's a video file, it will also detect any subtitles.
@ -238,6 +246,12 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil return nil
case strings.HasPrefix(mimetype, "image"): case strings.HasPrefix(mimetype, "image"):
i.Type = "image" i.Type = "image"
resolution, err := calculateImageResolution(i.Fs, i.Path)
if err != nil {
log.Printf("Error calculating image resolution: %v", err)
} else {
i.Resolution = resolution
}
return nil return nil
case strings.HasSuffix(mimetype, "pdf"): case strings.HasSuffix(mimetype, "pdf"):
i.Type = "pdf" i.Type = "pdf"
@ -266,6 +280,28 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
return nil return nil
} }
func calculateImageResolution(fs afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fs.Open(filePath)
if err != nil {
return nil, err
}
defer func() {
if cErr := file.Close(); cErr != nil {
log.Printf("Failed to close file: %v", cErr)
}
}()
config, _, err := image.DecodeConfig(file)
if err != nil {
return nil, err
}
return &ImageResolution{
Width: config.Width,
Height: config.Height,
}, nil
}
func (i *FileInfo) readFirstBytes() []byte { func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path) reader, err := i.Fs.Open(i.Path)
if err != nil { if err != nil {
@ -297,13 +333,21 @@ func (i *FileInfo) detectSubtitles() {
// detect multiple languages. Base*.vtt // detect multiple languages. Base*.vtt
// TODO: give subtitles descriptive names (lang) and track attributes // TODO: give subtitles descriptive names (lang) and track attributes
parentDir := strings.TrimRight(i.Path, i.Name) parentDir := strings.TrimRight(i.Path, i.Name)
dir, err := afero.ReadDir(i.Fs, parentDir) var dir []os.FileInfo
if err == nil { if len(i.currentDir) > 0 {
base := strings.TrimSuffix(i.Name, ext) dir = i.currentDir
for _, f := range dir { } else {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") { var err error
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name())) dir, err = afero.ReadDir(i.Fs, parentDir)
} if err != nil {
return
}
}
base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir {
if !f.IsDir() && strings.HasPrefix(f.Name(), base) && strings.HasSuffix(f.Name(), ".vtt") {
i.Subtitles = append(i.Subtitles, path.Join(parentDir, f.Name()))
} }
} }
} }
@ -349,16 +393,26 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
} }
file := &FileInfo{ file := &FileInfo{
Fs: i.Fs, Fs: i.Fs,
Name: name, Name: name,
Size: f.Size(), Size: f.Size(),
ModTime: f.ModTime(), ModTime: f.ModTime(),
Mode: f.Mode(), Mode: f.Mode(),
IsDir: f.IsDir(), IsDir: f.IsDir(),
IsSymlink: isSymlink, IsSymlink: isSymlink,
Link: symlink, Link: symlink,
Extension: filepath.Ext(name), Extension: filepath.Ext(name),
Path: fPath, Path: fPath,
currentDir: dir,
}
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") {
resolution, err := calculateImageResolution(file.Fs, file.Path)
if err != nil {
log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
} else {
file.Resolution = resolution
}
} }
if file.IsDir { if file.IsDir {

View File

@ -33,6 +33,7 @@ func Copy(fs afero.Fs, src, dst, scope string) error {
return err return err
} }
//nolint:exhaustive
switch info.Mode() & os.ModeType { switch info.Mode() & os.ModeType {
case os.ModeDir: case os.ModeDir:
return CopyDir(fs, src, dst, scope) return CopyDir(fs, src, dst, scope)

View File

@ -36,6 +36,7 @@ func CopyDir(fs afero.Fs, source, dest, scope string) error {
fsource := source + "/" + obj.Name() fsource := source + "/" + obj.Name()
fdest := dest + "/" + obj.Name() fdest := dest + "/" + obj.Name()
//nolint:exhaustive
switch obj.Mode() & os.ModeType { switch obj.Mode() & os.ModeType {
case os.ModeDir: case os.ModeDir:
// Create sub-directories, recursively. // Create sub-directories, recursively.

20
frontend/.eslintrc.json Normal file
View File

@ -0,0 +1,20 @@
{
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/eslint-config-prettier"
],
"rules": {
"vue/multi-word-component-names": "off",
"vue/no-reserved-component-names": "warn",
"vue/no-mutating-props": "warn"
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
}
}

2
frontend/.prettierignore Normal file
View File

@ -0,0 +1,2 @@
# Ignore artifacts:
dist

View File

@ -0,0 +1,3 @@
{
"trailingComma": "es5"
}

View File

@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/app"],
};

View File

@ -1,4 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

0
frontend/dist/.gitkeep vendored Normal file
View File

192
frontend/index.html Normal file
View File

@ -0,0 +1,192 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<title>File Browser</title>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta name="theme-color" content="#2979ff" />
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="/img/icons/mstile-144x144.png"
/>
<meta name="msapplication-TileColor" content="#2979ff" />
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = {
AuthMethod: "json",
BaseURL: "",
CSS: false,
Color: "",
DisableExternal: false,
DisableUsedPercentage: false,
EnableExec: true,
EnableThumbs: true,
LoginPage: true,
Name: "",
NoAuth: false,
ReCaptcha: false,
ResizePreview: true,
Signup: false,
StaticURL: "",
Theme: "",
TusSettings: { chunkSize: 10485760, retryCount: 5 },
Version: "(untracked)",
};
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-192x192.png"
),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-512x512.png"
),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

10
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

View File

@ -2,78 +2,59 @@
"name": "filebrowser-frontend", "name": "filebrowser-frontend",
"version": "2.0.0", "version": "2.0.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite dev",
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean", "serve": "vite serve",
"lint": "npx vue-cli-service lint --no-fix --max-warnings=0", "build": "vite build",
"fix": "npx vue-cli-service lint", "watch": "vite build --watch",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean" "clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"lint": "eslint --ext .vue,.js src/",
"lint:fix": "eslint --ext .vue,.js --fix src/",
"format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"ace-builds": "^1.5.1", "ace-builds": "^1.23.4",
"clipboard": "^2.0.4", "clipboard": "^2.0.11",
"core-js": "^3.9.1", "core-js": "^3.32.0",
"css-vars-ponyfill": "^2.4.3", "css-vars-ponyfill": "^2.4.8",
"js-base64": "^2.5.1", "filesize": "^10.0.8",
"js-base64": "^3.7.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"material-icons": "^1.10.5", "material-icons": "^1.13.9",
"moment": "^2.29.4", "moment": "^2.29.4",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"noty": "^3.2.0-beta", "noty": "^3.2.0-beta",
"pretty-bytes": "^6.0.0", "pretty-bytes": "^6.1.1",
"qrcode.vue": "^1.7.0", "qrcode.vue": "^1.7.0",
"tus-js-client": "^3.1.0", "tus-js-client": "^3.1.1",
"utif": "^3.1.0", "utif": "^3.1.0",
"vue": "^2.6.10", "vue": "^2.7.14",
"vue-async-computed": "^3.9.0", "vue-async-computed": "^3.9.0",
"vue-i18n": "^8.15.3", "vue-i18n": "^8.28.2",
"vue-lazyload": "^1.3.3", "vue-lazyload": "^1.3.5",
"vue-router": "^3.1.3", "vue-router": "^3.6.5",
"vue-simple-progress": "^1.1.1", "vue-simple-progress": "^1.1.1",
"vuex": "^3.1.2", "vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0", "vuex-router-sync": "^5.0.0",
"whatwg-fetch": "^3.6.2" "whatwg-fetch": "^3.6.17"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.5.4", "@vitejs/plugin-legacy": "^4.1.1",
"@vue/cli-plugin-babel": "^5.0.8", "@vitejs/plugin-vue2": "^2.2.0",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-prettier": "^8.0.0",
"compression-webpack-plugin": "^10.0.0", "autoprefixer": "^10.4.14",
"eslint": "^8.47.0", "eslint": "^8.46.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.16.1",
"file-loader": "^6.2.0", "jsdom": "^22.1.0",
"filesize": "^10.0.12", "postcss": "^8.4.31",
"prettier": "^3.0.2", "prettier": "^3.0.1",
"vue-template-compiler": "^2.6.10" "terser": "^5.19.2",
}, "vite": "^4.4.12",
"eslintConfig": { "vite-plugin-compression2": "^0.10.3",
"root": true, "vite-plugin-rewrite-all": "^1.0.1"
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended",
"@vue/prettier"
],
"rules": {
"vue/no-mutating-props": "off",
"vue/no-reserved-component-names": "off",
"vue/multi-word-component-names": "off"
},
"parserOptions": {
"parser": "@babel/eslint-parser",
"requireConfigFile": false
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View File

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

0
frontend/public/.gitkeep Normal file
View File

View File

@ -1,144 +1,193 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
[{[ if .ReCaptcha -]}] [{[ if .ReCaptcha -]}]
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script> <script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
[{[ end ]}] [{[ end ]}]
<title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title> <title>
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title>
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"> <link
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"> rel="icon"
type="image/png"
sizes="32x32"
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers --> <!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials"> <link
<meta name="theme-color" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"> rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta
name="theme-color"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Add to home screen for Safari on iOS/iPadOS --> <!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets"> <meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"> <link
rel="apple-touch-icon"
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
/>
<!-- Add to home screen for Windows --> <!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"> <meta
<meta name="msapplication-TileColor" content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"> name="msapplication-TileImage"
content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"
/>
<meta
name="msapplication-TileColor"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Inject Some Variables and generate the manifest json --> <!-- Inject Some Variables and generate the manifest json -->
<script> <script>
window.FileBrowser = JSON.parse('[{[ .Json ]}]'); // We can assign JSON directly
window.FileBrowser = [{[ .Json ]}];
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl("/img/icons/android-chrome-192x192.png"),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl("/img/icons/android-chrome-512x512.png"),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL; const stringManifest = JSON.stringify(dynamicManifest);
var dynamicManifest = { const blob = new Blob([stringManifest], { type: "application/json" });
"name": window.FileBrowser.Name || 'File Browser', const manifestURL = URL.createObjectURL(blob);
"short_name": window.FileBrowser.Name || 'File Browser', document
"icons": [ .querySelector("#manifestPlaceholder")
{ .setAttribute("href", manifestURL);
"src": fullStaticURL + "/img/icons/android-chrome-192x192.png", </script>
"sizes": "192x192",
"type": "image/png" <style>
}, #loading {
{ position: fixed;
"src": fullStaticURL + "/img/icons/android-chrome-512x512.png", top: 0;
"sizes": "512x512", left: 0;
"type": "image/png" width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
} }
], 40% {
"start_url": window.location.origin + window.FileBrowser.BaseURL, -webkit-transform: scale(1);
"display": "standalone", }
"background_color": "#ffffff", }
"theme_color": window.FileBrowser.Color || "#455a64"
}
const stringManifest = JSON.stringify(dynamicManifest); @keyframes sk-bouncedelay {
const blob = new Blob([stringManifest], {type: 'application/json'}); 0%,
const manifestURL = URL.createObjectURL(blob); 80%,
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL); 100% {
</script> -webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<style> <div id="loading">
#loading { <div class="spinner">
position: fixed; <div class="bounce1"></div>
top: 0; <div class="bounce2"></div>
left: 0; <div class="bounce3"></div>
width: 100%; </div>
height: 100%;
background: #fff;
z-index: 9999;
transition: .1s ease opacity;
-webkit-transition: .1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes sk-bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div> </div>
</div>
[{[ if .Theme -]}] <script type="module" src="/src/main.js"></script>
<link rel="stylesheet" href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css" />
[{[ end ]}] [{[ if .Theme -]}]
[{[ if .CSS -]}] <link
rel="stylesheet"
href="[{[ .StaticURL ]}]/themes/[{[ .Theme ]}].css"
/>
[{[ end ]}] [{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" /> <link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}] [{[ end ]}]
</body> </body>
</html> </html>

View File

@ -177,6 +177,12 @@ table th {
background: var(--surfacePrimary); background: var(--surfacePrimary);
color: var(--textPrimary); color: var(--textPrimary);
} }
.shell__divider {
background: rgba(255, 255, 255, 0.1);
}
.shell__divider:hover {
background: rgba(255, 255, 255, 0.4);
}
.shell__result { .shell__result {
border-top: 1px solid var(--divider); border-top: 1px solid var(--divider);
} }

View File

@ -6,7 +6,7 @@
<script> <script>
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
__webpack_public_path__ = window.FileBrowser.StaticURL + "/"; // __webpack_public_path__ = window.FileBrowser.StaticURL + "/";
export default { export default {
name: "app", name: "app",

View File

@ -6,6 +6,12 @@ import { fetchURL } from "./utils";
const RETRY_BASE_DELAY = 1000; const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000; const RETRY_MAX_DELAY = 20000;
const SPEED_UPDATE_INTERVAL = 1000;
const ALPHA = 0.2;
const ONE_MINUS_ALPHA = 1 - ALPHA;
const RECENT_SPEEDS_LIMIT = 5;
const MB_DIVISOR = 1024 * 1024;
const CURRENT_UPLOAD_LIST = {};
export async function upload( export async function upload(
filePath, filePath,
@ -34,19 +40,47 @@ export async function upload(
"X-Auth": store.state.jwt, "X-Auth": store.state.jwt,
}, },
onError: function (error) { onError: function (error) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
reject("Upload failed: " + error); reject("Upload failed: " + error);
}, },
onProgress: function (bytesUploaded) { onProgress: function (bytesUploaded) {
// Emulate ProgressEvent.loaded which is used by calling functions let fileData = CURRENT_UPLOAD_LIST[filePath];
// loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded) fileData.currentBytesUploaded = bytesUploaded;
if (!fileData.hasStarted) {
fileData.hasStarted = true;
fileData.lastProgressTimestamp = Date.now();
fileData.interval = setInterval(() => {
calcProgress(filePath);
}, SPEED_UPDATE_INTERVAL);
}
if (typeof onupload === "function") { if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded }); onupload({ loaded: bytesUploaded });
} }
}, },
onSuccess: function () { onSuccess: function () {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
delete CURRENT_UPLOAD_LIST[filePath];
resolve(); resolve();
}, },
}); });
CURRENT_UPLOAD_LIST[filePath] = {
upload: upload,
recentSpeeds: [],
initialBytesUploaded: 0,
currentBytesUploaded: 0,
currentAverageSpeed: 0,
lastProgressTimestamp: null,
sumOfRecentSpeeds: 0,
hasStarted: false,
interval: null,
};
upload.start(); upload.start();
}); });
} }
@ -88,3 +122,74 @@ export async function useTus(content) {
function isTusSupported() { function isTusSupported() {
return tus.isSupported === true; return tus.isSupported === true;
} }
function computeETA(state) {
if (state.speedMbyte === 0) {
return Infinity;
}
const totalSize = state.sizes.reduce((acc, size) => acc + size, 0);
const uploadedSize = state.progress.reduce(
(acc, progress) => acc + progress,
0
);
const remainingSize = totalSize - uploadedSize;
const speedBytesPerSecond = state.speedMbyte * 1024 * 1024;
return remainingSize / speedBytesPerSecond;
}
function computeGlobalSpeedAndETA() {
let totalSpeed = 0;
let totalCount = 0;
for (let filePath in CURRENT_UPLOAD_LIST) {
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
totalCount++;
}
if (totalCount === 0) return { speed: 0, eta: Infinity };
const averageSpeed = totalSpeed / totalCount;
const averageETA = computeETA(store.state.upload, averageSpeed);
return { speed: averageSpeed, eta: averageETA };
}
function calcProgress(filePath) {
let fileData = CURRENT_UPLOAD_LIST[filePath];
let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000;
let bytesSinceLastUpdate =
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift();
}
fileData.recentSpeeds.push(currentSpeed);
fileData.sumOfRecentSpeeds += currentSpeed;
let avgRecentSpeed =
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
fileData.currentAverageSpeed =
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
const { speed, eta } = computeGlobalSpeedAndETA();
store.commit("setUploadSpeed", speed);
store.commit("setETA", eta);
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
fileData.lastProgressTimestamp = Date.now();
}
export function abortAllUploads() {
for (let filePath in CURRENT_UPLOAD_LIST) {
if (CURRENT_UPLOAD_LIST[filePath].interval) {
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
}
if (CURRENT_UPLOAD_LIST[filePath].upload) {
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
}
delete CURRENT_UPLOAD_LIST[filePath];
}
}

View File

@ -33,7 +33,7 @@
</template> </template>
<script> <script>
import { filesize } from "filesize"; import { filesize } from "@/utils";
import { mapState } from "vuex"; import { mapState } from "vuex";
import ProgressBar from "vue-simple-progress"; import ProgressBar from "vue-simple-progress";

View File

@ -90,10 +90,10 @@ export default {
}; };
}, },
watch: { watch: {
show(val, old) { currentPrompt(val, old) {
this.active = val === "search"; this.active = val?.prompt === "search";
if (old === "search" && !this.active) { if (old?.prompt === "search" && !this.active) {
if (this.reload) { if (this.reload) {
this.setReload(true); this.setReload(true);
} }
@ -116,8 +116,8 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(["user", "show"]), ...mapState(["user"]),
...mapGetters(["isListing"]), ...mapGetters(["isListing", "currentPrompt"]),
boxes() { boxes() {
return boxes; return boxes;
}, },

View File

@ -1,37 +1,54 @@
<template> <template>
<div <div
@click="focus"
class="shell" class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }" :class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em` }"
> >
<div v-for="(c, index) in content" :key="index" class="shell__result"> <div
<div class="shell__prompt"> @pointerdown="startDrag()"
<i class="material-icons">chevron_right</i> @pointerup="stopDrag()"
class="shell__divider"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
></div>
<div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div> </div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }"> <div
<div class="shell__prompt"> class="shell__result"
<i class="material-icons">chevron_right</i> :class="{ 'shell__result--hidden': !canInput }"
>
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre
tabindex="0"
ref="input"
class="shell__text"
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit"
/>
</div> </div>
<pre
tabindex="0"
ref="input"
class="shell__text"
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit"
/>
</div> </div>
<div
@pointerup="stopDrag()"
class="shell__overlay"
v-show="this.shellDrag"
></div>
</div> </div>
</template> </template>
<script> <script>
import { mapMutations, mapState, mapGetters } from "vuex"; import { mapMutations, mapState, mapGetters } from "vuex";
import { commands } from "@/api"; import { commands } from "@/api";
import { throttle } from "lodash";
import { theme } from "@/utils/constants";
export default { export default {
name: "shell", name: "shell",
@ -51,9 +68,55 @@ export default {
history: [], history: [],
historyPos: 0, historyPos: 0,
canInput: true, canInput: true,
shellDrag: false,
shellHeight: 25,
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}), }),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeDestroy() {
window.removeEventListener("resize", this.resize);
},
methods: { methods: {
...mapMutations(["toggleShell"]), ...mapMutations(["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) {
this.shellHeight = userPos.toFixed(2);
}
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) {
this.shellHeight = top;
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
scroll: function () { scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight; this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
}, },

View File

@ -144,7 +144,7 @@
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import * as auth from "@/utils/auth"; import * as auth from "@/utils/auth";
import Quota from "@/components/Quota"; import Quota from "./Quota.vue";
import { import {
version, version,
signup, signup,
@ -170,9 +170,9 @@ export default {
}, },
computed: { computed: {
...mapState(["user"]), ...mapState(["user"]),
...mapGetters(["isLogged"]), ...mapGetters(["isLogged", "currentPrompt"]),
active() { active() {
return this.$store.state.show === "sidebar"; return this.currentPrompt?.prompt === "sidebar";
}, },
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,

View File

@ -84,7 +84,7 @@
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import { files as api } from "@/api"; import { files as api } from "@/api";
import Action from "@/components/header/Action"; import Action from "../header/Action.vue";
export default { export default {
name: "context-menu", name: "context-menu",

View File

@ -10,12 +10,7 @@
@mouseup="mouseUp" @mouseup="mouseUp"
@wheel="wheelMove" @wheel="wheelMove"
> >
<img <img class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad" />
src=""
class="image-ex-img image-ex-img-center"
ref="imgex"
@load="onLoad"
/>
</div> </div>
</template> </template>
<script> <script>

View File

@ -51,7 +51,7 @@
<script> <script>
import { enableThumbs } from "@/utils/constants"; import { enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex"; import { mapMutations, mapGetters, mapState } from "vuex";
import { filesize } from "filesize"; import { filesize } from "@/utils";
import moment from "moment"; import moment from "moment";
import { files as api } from "@/api"; import { files as api } from "@/api";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";

View File

@ -11,7 +11,7 @@
<slot /> <slot />
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }"> <div id="dropdown" :class="{ active: this.currentPromptName === 'more' }">
<slot name="actions" /> <slot name="actions" />
</div> </div>
@ -25,7 +25,7 @@
<div <div
class="overlay" class="overlay"
v-show="this.$store.state.show == 'more'" v-show="this.currentPromptName == 'more'"
@click="$store.commit('closeHovers')" @click="$store.commit('closeHovers')"
/> />
</header> </header>
@ -34,7 +34,8 @@
<script> <script>
import { logoURL } from "@/utils/constants"; import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action"; import Action from "@/components/header/Action.vue";
import { mapGetters } from "vuex";
export default { export default {
name: "header-bar", name: "header-bar",
@ -52,6 +53,9 @@ export default {
this.$store.commit("showHover", "sidebar"); this.$store.commit("showHover", "sidebar");
}, },
}, },
computed: {
...mapGetters(["currentPromptName"]),
},
}; };
</script> </script>

View File

@ -6,33 +6,50 @@
<div class="card-content"> <div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list @update:selected="(val) => (dest = val)"></file-list> <file-list ref="fileList" @update:selected="(val) => (dest = val)">
</file-list>
</div> </div>
<div class="card-action"> <div
<button class="card-action"
class="button button--flat button--grey" style="display: flex; align-items: center; justify-content: space-between;"
@click="$store.commit('closeHovers')" >
:aria-label="$t('buttons.cancel')" <template v-if="user.perm.create">
:title="$t('buttons.cancel')" <button
> class="button button--flat"
{{ $t("buttons.cancel") }} @click="$refs.fileList.createDir()"
</button> :aria-label="$t('sidebar.newFolder')"
<button :title="$t('sidebar.newFolder')"
class="button button--flat" style="justify-self: left;"
@click="copy" >
:aria-label="$t('buttons.copy')" <span>{{ $t("sidebar.newFolder") }}</span>
:title="$t('buttons.copy')" </button>
> </template>
{{ $t("buttons.copy") }} <div>
</button> <button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
>
{{ $t("buttons.copy") }}
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapState } from "vuex";
import FileList from "./FileList"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
@ -46,7 +63,7 @@ export default {
dest: null, dest: null,
}; };
}, },
computed: mapState(["req", "selected"]), computed: mapState(["req", "selected", "user"]),
methods: { methods: {
copy: async function (event) { copy: async function (event) {
event.preventDefault(); event.preventDefault();

View File

@ -47,8 +47,8 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(["isListing", "selectedCount"]), ...mapGetters(["isListing", "selectedCount", "currentPrompt"]),
...mapState(["req", "selected", "showConfirm"]), ...mapState(["req", "selected"]),
trashBinCheckbox() { trashBinCheckbox() {
if (trashDir === "") { if (trashDir === "") {
return false; return false;
@ -81,7 +81,7 @@ export default {
await api.remove(this.$route.path, this.skipTrash); await api.remove(this.$route.path, this.skipTrash);
buttons.success("delete"); buttons.success("delete");
this.showConfirm(); this.currentPrompt?.confirm();
this.closeHovers(); this.closeHovers();
return; return;
} }

View File

@ -11,7 +11,7 @@
v-for="(ext, format) in formats" v-for="(ext, format) in formats"
:key="format" :key="format"
class="button button--block" class="button button--block"
@click="showConfirm(format)" @click="currentPrompt.confirm(format)"
v-focus v-focus
> >
{{ ext }} {{ ext }}
@ -21,7 +21,7 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapGetters } from "vuex";
export default { export default {
name: "download", name: "download",
@ -38,6 +38,8 @@ export default {
}, },
}; };
}, },
computed: mapState(["showConfirm"]), computed: {
...mapGetters(["currentPrompt"]),
},
}; };
</script> </script>

View File

@ -133,6 +133,17 @@ export default {
this.selected = event.currentTarget.dataset.url; this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected); this.$emit("update:selected", this.selected);
}, },
createDir: async function () {
this.$store.commit("showHover", {
prompt: "newDir",
action: null,
confirm: null,
props: {
redirect: false,
base: this.current === this.$route.path ? null : this.current,
},
});
},
}, },
}; };
</script> </script>

View File

@ -12,16 +12,23 @@
<p class="break-word" v-if="selected.length < 2"> <p class="break-word" v-if="selected.length < 2">
<strong>{{ $t("prompts.displayName") }}</strong> {{ name }} <strong>{{ $t("prompts.displayName") }}</strong> {{ name }}
</p> </p>
<p v-if="!dir">
<p v-if="!dir || selected.length > 1">
<strong>{{ $t("prompts.size") }}:</strong> <strong>{{ $t("prompts.size") }}:</strong>
<span id="content_length"></span> {{ humanSize }} <span id="content_length"></span> {{ humanSize }}
</p> </p>
<p v-if="dir"> <p v-if="dir">
<strong>{{ $t("prompts.size") }}: </strong> <strong>{{ $t("prompts.size") }}: </strong>
<code> <code>
<a @click="diskUsage($event)">{{ $t("prompts.show") }}</a> <a @click="diskUsage($event)">{{ $t("prompts.show") }}</a>
</code> </code>
</p> </p>
<div v-if="resolution">
<strong>{{ $t("prompts.resolution") }}:</strong>
{{ resolution.width }} x {{ resolution.height }}
</div>
<p v-if="selected.length < 2" :title="modTime"> <p v-if="selected.length < 2" :title="modTime">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }} <strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</p> </p>
@ -87,7 +94,7 @@
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import { filesize } from "filesize"; import { filesize } from "@/utils";
import moment from "moment"; import moment from "moment";
import { files as api } from "@/api"; import { files as api } from "@/api";
@ -132,6 +139,18 @@ export default {
: this.req.items[this.selected[0]].isDir) : this.req.items[this.selected[0]].isDir)
); );
}, },
resolution: function() {
if (this.selectedCount === 1) {
const selectedItem = this.req.items[this.selected[0]];
if (selectedItem && selectedItem.type === 'image') {
return selectedItem.resolution;
}
}
else if (this.req && this.req.type === 'image') {
return this.req.resolution;
}
return null;
},
}, },
methods: { methods: {
checksum: async function (event, algo) { checksum: async function (event, algo) {
@ -148,7 +167,7 @@ export default {
try { try {
const hash = await api.checksum(link, algo); const hash = await api.checksum(link, algo);
// eslint-disable-next-line // eslint-disable-next-line
event.target.innerHTML = hash event.target.innerHTML = hash;
} catch (e) { } catch (e) {
this.$showError(e); this.$showError(e);
} }

View File

@ -5,34 +5,51 @@
</div> </div>
<div class="card-content"> <div class="card-content">
<file-list @update:selected="(val) => (dest = val)"></file-list> <file-list ref="fileList" @update:selected="(val) => (dest = val)">
</file-list>
</div> </div>
<div class="card-action"> <div
<button class="card-action"
class="button button--flat button--grey" style="display: flex; align-items: center; justify-content: space-between;"
@click="$store.commit('closeHovers')" >
:aria-label="$t('buttons.cancel')" <template v-if="user.perm.create">
:title="$t('buttons.cancel')" <button
> class="button button--flat"
{{ $t("buttons.cancel") }} @click="$refs.fileList.createDir()"
</button> :aria-label="$t('sidebar.newFolder')"
<button :title="$t('sidebar.newFolder')"
class="button button--flat" style="justify-self: left;"
@click="move" >
:disabled="$route.path === dest" <span>{{ $t("sidebar.newFolder") }}</span>
:aria-label="$t('buttons.move')" </button>
:title="$t('buttons.move')" </template>
> <div>
{{ $t("buttons.move") }} <button
</button> class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')"
>
{{ $t("buttons.move") }}
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapState } from "vuex";
import FileList from "./FileList"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
@ -46,7 +63,7 @@ export default {
dest: null, dest: null,
}; };
}, },
computed: mapState(["req", "selected"]), computed: mapState(["req", "selected", "user"]),
methods: { methods: {
move: async function (event) { move: async function (event) {
event.preventDefault(); event.preventDefault();

View File

@ -43,6 +43,16 @@ import url from "@/utils/url";
export default { export default {
name: "new-dir", name: "new-dir",
props: {
redirect: {
type: Boolean,
default: true,
},
base: {
type: [String, null],
default: null,
},
},
data: function () { data: function () {
return { return {
name: "", name: "",
@ -57,7 +67,11 @@ export default {
if (this.new === "") return; if (this.new === "") return;
// Build the path of the new directory. // Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + "/" : "/"; let uri;
if (this.base) uri = this.base;
else if (this.isFiles) uri = this.$route.path + "/";
else uri = "/";
if (!this.isListing) { if (!this.isListing) {
uri = url.removeLastDir(uri) + "/"; uri = url.removeLastDir(uri) + "/";
@ -65,10 +79,14 @@ export default {
uri += encodeURIComponent(this.name) + "/"; uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/"); uri = uri.replace("//", "/");
try { try {
await api.post(uri); await api.post(uri);
this.$router.push({ path: uri }); if (this.redirect) {
this.$router.push({ path: uri });
} else if (!this.base) {
const res = await api.fetch(url.removeLastDir(uri) + "/");
this.$store.commit("updateRequest", res);
}
} catch (e) { } catch (e) {
this.$showError(e); this.$showError(e);
} }

View File

@ -1,29 +1,36 @@
<template> <template>
<div> <div>
<component ref="currentComponent" :is="currentComponent"></component> <component
v-if="showOverlay"
:ref="currentPromptName"
:is="currentPromptName"
v-bind="currentPrompt.props"
>
</component>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div> <div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div> </div>
</template> </template>
<script> <script>
import Help from "./Help"; import Help from "./Help.vue";
import Info from "./Info"; import Info from "./Info.vue";
import Delete from "./Delete"; import Delete from "./Delete.vue";
import Rename from "./Rename"; import Rename from "./Rename.vue";
import Download from "./Download"; import Download from "./Download.vue";
import Move from "./Move"; import Move from "./Move.vue";
import Archive from "./Archive"; import Archive from "./Archive.vue";
import Unarchive from "./Unarchive"; import Unarchive from "./Unarchive.vue";
import Permissions from "./Permissions"; import Permissions from "./Permissions.vue";
import Copy from "./Copy"; import Copy from "./Copy.vue";
import NewFile from "./NewFile"; import NewFile from "./NewFile.vue";
import NewDir from "./NewDir"; import NewDir from "./NewDir.vue";
import Replace from "./Replace"; import Replace from "./Replace.vue";
import ReplaceRename from "./ReplaceRename"; import ReplaceRename from "./ReplaceRename.vue";
import Share from "./Share"; import Share from "./Share.vue";
import Upload from "./Upload"; import Upload from "./Upload.vue";
import ShareDelete from "./ShareDelete"; import ShareDelete from "./ShareDelete.vue";
import { mapState } from "vuex"; import Sidebar from "../Sidebar.vue";
import { mapGetters, mapState } from "vuex";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
export default { export default {
@ -46,6 +53,7 @@ export default {
ReplaceRename, ReplaceRename,
Upload, Upload,
ShareDelete, ShareDelete,
Sidebar
}, },
data: function () { data: function () {
return { return {
@ -58,7 +66,7 @@ export default {
}, },
created() { created() {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
if (this.show == null) return; if (this.currentPrompt == null) return;
let prompt = this.$refs.currentComponent; let prompt = this.$refs.currentComponent;
@ -70,7 +78,7 @@ export default {
// Enter // Enter
if (event.keyCode == 13) { if (event.keyCode == 13) {
switch (this.show) { switch (this.currentPrompt.prompt) {
case "delete": case "delete":
prompt.submit(); prompt.submit();
break; break;
@ -88,34 +96,13 @@ export default {
}); });
}, },
computed: { computed: {
...mapState(["show", "plugins"]), ...mapState(["plugins"]),
currentComponent: function () { ...mapGetters(["currentPrompt", "currentPromptName"]),
const matched =
[
"info",
"help",
"delete",
"rename",
"move",
"archive",
"unarchive",
"permissions",
"copy",
"newFile",
"newDir",
"download",
"replace",
"replace-rename",
"share",
"upload",
"share-delete",
].indexOf(this.show) >= 0;
return (matched && this.show) || null;
},
showOverlay: function () { showOverlay: function () {
return ( return (
this.show !== null && this.show !== "search" && this.show !== "more" this.currentPrompt !== null &&
this.currentPrompt.prompt !== "search" &&
this.currentPrompt.prompt !== "more"
); );
}, },
}, },

View File

@ -19,7 +19,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="showAction" @click="currentPrompt.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
> >
@ -27,7 +27,7 @@
</button> </button>
<button <button
class="button button--flat button--red" class="button button--flat button--red"
@click="showConfirm" @click="currentPrompt.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
> >
@ -38,10 +38,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapGetters } from "vuex";
export default { export default {
name: "replace", name: "replace",
computed: mapState(["showConfirm", "showAction"]), computed: mapGetters(["currentPrompt"]),
}; };
</script> </script>

View File

@ -19,7 +19,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="(event) => showConfirm(event, 'rename')" @click="(event) => currentPrompt.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')" :title="$t('buttons.rename')"
> >
@ -27,7 +27,7 @@
</button> </button>
<button <button
class="button button--flat button--red" class="button button--flat button--red"
@click="(event) => showConfirm(event, 'overwrite')" @click="(event) => currentPrompt.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
> >
@ -38,10 +38,10 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapGetters } from "vuex";
export default { export default {
name: "replace-rename", name: "replace-rename",
computed: mapState(["showConfirm"]), computed: mapGetters(["currentPrompt"]),
}; };
</script> </script>

View File

@ -25,16 +25,16 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapGetters } from "vuex";
export default { export default {
name: "share-delete", name: "share-delete",
computed: { computed: {
...mapState(["showConfirm"]), ...mapGetters(["currentPrompt"]),
}, },
methods: { methods: {
submit: function () { submit: function () {
this.showConfirm(); this.currentPrompt?.confirm();
}, },
}, },
}; };

View File

@ -50,12 +50,12 @@
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import FileList from "./FileList"; import FileList from "./FileList.vue";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
export default { export default {
name: "rename", name: "unarchive",
components: { FileList }, components: { FileList },
data: function () { data: function () {
return { return {

View File

@ -7,7 +7,18 @@
<div class="card floating"> <div class="card floating">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2> <h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
<div class="upload-info">
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
<div class="upload-eta">{{ formattedETA }} remaining</div>
</div>
<button
class="action"
@click="abortAll"
aria-label="Abort upload"
title="Abort upload"
>
<i class="material-icons">{{ "cancel" }}</i>
</button>
<button <button
class="action" class="action"
@click="toggle" @click="toggle"
@ -42,7 +53,9 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import { mapGetters, mapMutations } from "vuex";
import { abortAllUploads } from "@/api/tus";
import buttons from "@/utils/buttons";
export default { export default {
name: "uploadFiles", name: "uploadFiles",
@ -52,12 +65,42 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(["filesInUpload", "filesInUploadCount"]), ...mapGetters([
"filesInUpload",
"filesInUploadCount",
"uploadSpeed",
"eta",
]),
...mapMutations(["resetUpload"]),
formattedETA() {
if (!this.eta || this.eta === Infinity) {
return "--:--:--";
}
let totalSeconds = this.eta;
const hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds % 60);
return `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
},
}, },
methods: { methods: {
toggle: function () { toggle: function () {
this.open = !this.open; this.open = !this.open;
}, },
abortAll() {
if (confirm(this.$t("upload.abortUpload"))) {
abortAllUploads();
buttons.done("upload");
this.open = false;
this.$store.commit("resetUpload");
this.$store.commit("setReload", true);
}
},
}, },
}; };
</script> </script>

View File

@ -67,10 +67,10 @@
</template> </template>
<script> <script>
import Languages from "./Languages"; import Languages from "./Languages.vue";
import Rules from "./Rules"; import Rules from "./Rules.vue";
import Permissions from "./Permissions"; import Permissions from "./Permissions.vue";
import Commands from "./Commands"; import Commands from "./Commands.vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
export default { export default {

View File

@ -2,21 +2,49 @@
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
height: 25em;
max-height: calc(100% - 4em); max-height: calc(100% - 4em);
background: white; background: white;
color: #212121; color: #212121;
z-index: 9999; z-index: 9997;
width: 100%; width: 100%;
font-family: monospace;
overflow: auto;
font-size: 1rem;
cursor: text;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .2s ease transform; transition: .2s ease transform;
} }
body.rtl .shell { .shell__divider {
position: relative;
height: 8px;
z-index: 9999;
background: rgba(127, 127, 127, 0.1);
transition: 0.2s ease background;
cursor: ns-resize;
touch-action: none;
user-select: none;
}
.shell__divider:hover {
background: rgba(127, 127, 127, 0.4);
}
.shell__content {
height: 100%;
font-family: monospace;
overflow: auto;
font-size: 1rem;
cursor: text;
}
.shell__overlay {
position: fixed;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
z-index: 9998;
background-color: rgba(0, 0, 0, 0.05);
}
body.rtl .shell-content {
direction: ltr; direction: ltr;
} }

View File

@ -1,173 +1,242 @@
@import "material-icons/iconfont/filled.css";
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format('woff2'); src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-cyrillic-ext.woff2) format("woff2");
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-cyrillic.woff2) format('woff2'); src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-cyrillic.woff2) format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek-ext.woff2) format('woff2'); src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-greek-ext.woff2) format("woff2");
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-greek.woff2) format('woff2'); src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-greek.woff2) format("woff2");
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-vietnamese.woff2) format('woff2'); src:
local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-vietnamese.woff2) format("woff2");
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin-ext.woff2) format('woff2'); src:
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F,
U+A720-A7FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(../assets/fonts/roboto/normal-latin.woff2) format('woff2'); src:
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; local("Roboto"),
local("Roboto-Regular"),
url(../assets/fonts/roboto/normal-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format('woff2'); src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-cyrillic-ext.woff2) format("woff2");
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-cyrillic.woff2) format('woff2'); src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-cyrillic.woff2) format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek-ext.woff2) format('woff2'); src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-greek-ext.woff2) format("woff2");
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-greek.woff2) format('woff2'); src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-greek.woff2) format("woff2");
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-vietnamese.woff2) format('woff2'); src:
local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-vietnamese.woff2) format("woff2");
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin-ext.woff2) format('woff2'); src:
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F,
U+A720-A7FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'), url(../assets/fonts/roboto/medium-latin.woff2) format('woff2'); src:
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; local("Roboto Medium"),
local("Roboto-Medium"),
url(../assets/fonts/roboto/medium-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-cyrillic-ext.woff2) format('woff2'); src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-cyrillic-ext.woff2) format("woff2");
unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-cyrillic.woff2) format('woff2'); src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-cyrillic.woff2) format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-greek-ext.woff2) format('woff2'); src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-greek-ext.woff2) format("woff2");
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-greek.woff2) format('woff2'); src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-greek.woff2) format("woff2");
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-vietnamese.woff2) format('woff2'); src:
local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-vietnamese.woff2) format("woff2");
unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB; unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-latin-ext.woff2) format('woff2'); src:
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F,
U+A720-A7FF;
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'), url(../assets/fonts/roboto/bold-latin.woff2) format('woff2'); src:
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; local("Roboto Bold"),
local("Roboto-Bold"),
url(../assets/fonts/roboto/bold-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
} }
@import '~material-icons/iconfont/filled.css';
.material-icons { .material-icons {
font-size: 1.5rem; font-size: 1.5rem;
} }

View File

@ -126,6 +126,10 @@
right: 0; right: 0;
} }
.shell__divider {
height: 12px;
}
header .search-button, header .search-button,
header .menu-button { header .menu-button {
display: inherit; display: inherit;

View File

@ -1,6 +1,6 @@
@import "~normalize.css/normalize.css"; @import "normalize.css/normalize.css";
@import "~noty/lib/noty.css"; @import "noty/lib/noty.css";
@import "~noty/lib/themes/mint.css"; @import "noty/lib/themes/mint.css";
@import "./_variables.css"; @import "./_variables.css";
@import "./_buttons.css"; @import "./_buttons.css";
@import "./_inputs.css"; @import "./_inputs.css";
@ -14,6 +14,7 @@
@import "./upload-files.css"; @import "./upload-files.css";
@import "./dashboard.css"; @import "./dashboard.css";
@import "./login.css"; @import "./login.css";
@import "./mobile.css";
.link { .link {
color: var(--blue); color: var(--blue);
@ -27,9 +28,9 @@ main .spinner {
} }
main .spinner > div { main .spinner > div {
width: .8em; width: 0.8em;
height: .8em; height: 0.8em;
margin: 0 .1em; margin: 0 0.1em;
font-size: 1em; font-size: 1em;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
border-radius: 100%; border-radius: 100%;
@ -71,7 +72,7 @@ main .spinner .bounce2 {
transition: 0.2s ease all; transition: 0.2s ease all;
border: 0; border: 0;
margin: 0; margin: 0;
color: #546E7A; color: #546e7a;
border-radius: 50%; border-radius: 50%;
background: transparent; background: transparent;
padding: 0; padding: 0;
@ -88,12 +89,12 @@ main .spinner .bounce2 {
.action i { .action i {
padding: 0.4em; padding: 0.4em;
transition: .1s ease-in-out all; transition: 0.1s ease-in-out all;
border-radius: 50%; border-radius: 50%;
} }
.action:hover { .action:hover {
background-color: rgba(0, 0, 0, .1); background-color: rgba(0, 0, 0, 0.1);
} }
.action ul { .action ul {
@ -109,8 +110,8 @@ main .spinner .bounce2 {
.action ul li { .action ul li {
line-height: 1; line-height: 1;
padding: .7em; padding: 0.7em;
transition: .1s ease background-color; transition: 0.1s ease background-color;
} }
.action ul li:hover { .action ul li:hover {
@ -139,7 +140,7 @@ main .spinner .bounce2 {
background: var(--blue); background: var(--blue);
color: #fff; color: #fff;
border-radius: 50%; border-radius: 50%;
font-size: .75em; font-size: 0.75em;
width: 1.8em; width: 1.8em;
height: 1.8em; height: 1.8em;
text-align: center; text-align: center;
@ -148,7 +149,6 @@ main .spinner .bounce2 {
border: 2px solid white; border: 2px solid white;
} }
/* PREVIEWER */ /* PREVIEWER */
#previewer { #previewer {
@ -179,7 +179,7 @@ main .spinner .bounce2 {
} }
#previewer header .action:hover { #previewer header .action:hover {
background-color: rgba(255, 255, 255, 0.3) background-color: rgba(255, 255, 255, 0.3);
} }
#previewer header .action span { #previewer header .action span {
@ -220,14 +220,14 @@ main .spinner .bounce2 {
} }
#previewer .preview .info .title i { #previewer .preview .info .title i {
display: block; display: block;
margin-bottom: .1em; margin-bottom: 0.1em;
font-size: 4em; font-size: 4em;
} }
#previewer .preview .info .button { #previewer .preview .info .button {
display: inline-block; display: inline-block;
} }
#previewer .preview .info .button:hover { #previewer .preview .info .button:hover {
background-color: rgba(255, 255, 255, 0.2) background-color: rgba(255, 255, 255, 0.2);
} }
#previewer .preview .info .button i { #previewer .preview .info .button i {
display: block; display: block;
@ -241,15 +241,15 @@ main .spinner .bounce2 {
} }
#previewer h2.message { #previewer h2.message {
color: rgba(255, 255, 255, 0.5) color: rgba(255, 255, 255, 0.5);
} }
#previewer>button { #previewer > button {
margin: 0; margin: 0;
position: fixed; position: fixed;
top: calc(50% + 1.85em); top: calc(50% + 1.85em);
transform: translateY(-50%); transform: translateY(-50%);
background-color: rgba(80, 80, 80, .5); background-color: rgba(80, 80, 80, 0.5);
color: white; color: white;
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
@ -259,20 +259,20 @@ main .spinner .bounce2 {
transition: 0.2s ease all; transition: 0.2s ease all;
} }
#previewer>button.hidden { #previewer > button.hidden {
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
} }
#previewer>button>i { #previewer > button > i {
padding: 0.4em; padding: 0.4em;
} }
#previewer>button:first-of-type { #previewer > button:first-of-type {
left: 0.5em; left: 0.5em;
} }
#previewer>button:last-of-type { #previewer > button:last-of-type {
right: 0.5em; right: 0.5em;
} }
@ -321,7 +321,7 @@ body.rtl .breadcrumbs .chevron {
} }
#editor-container .breadcrumbs span { #editor-container .breadcrumbs span {
font-size: .75rem; font-size: 0.75rem;
} }
#editor-container .breadcrumbs i { #editor-container .breadcrumbs i {
@ -339,7 +339,7 @@ body.rtl .breadcrumbs .chevron {
.noty_buttons button { .noty_buttons button {
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0,0,0,0.1); border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 0; box-shadow: 0 0 0 0;
font-size: 1rem; font-size: 1rem;
} }
@ -356,7 +356,7 @@ body.rtl .breadcrumbs .chevron {
.credits > span { .credits > span {
display: block; display: block;
margin: .3em 0; margin: 0.3em 0;
} }
.credits a, .credits a,
@ -365,7 +365,6 @@ body.rtl .breadcrumbs .chevron {
cursor: pointer; cursor: pointer;
} }
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* ANIMATIONS * * ANIMATIONS *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */
@ -393,20 +392,20 @@ body.rtl .breadcrumbs .chevron {
.rules > div { .rules > div {
display: flex; display: flex;
align-items: center; align-items: center;
margin: .5rem 0; margin: 0.5rem 0;
} }
.rules input[type="checkbox"] { .rules input[type="checkbox"] {
margin-right: .2rem; margin-right: 0.2rem;
} }
.rules input[type="text"] { .rules input[type="text"] {
border: 1px solid#ddd; border: 1px solid#ddd;
padding: .2rem; padding: 0.2rem;
} }
.rules label { .rules label {
margin-right: .5rem; margin-right: 0.5rem;
} }
.rules button { .rules button {
@ -414,12 +413,10 @@ body.rtl .breadcrumbs .chevron {
} }
.rules button.delete { .rules button.delete {
padding: .2rem .5rem; padding: 0.2rem 0.5rem;
margin-left: .5rem; margin-left: 0.5rem;
} }
@import './mobile.css';
/* * * * * * * * * * * * * * * * /* * * * * * * * * * * * * * * *
* RTL overrides * * RTL overrides *
* * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * */

View File

@ -50,6 +50,9 @@
"downloadFolder": "Download Folder", "downloadFolder": "Download Folder",
"downloadSelected": "Download Selected" "downloadSelected": "Download Selected"
}, },
"upload": {
"abortUpload": "Are you sure you wish to abort?"
},
"errors": { "errors": {
"connection": "The server can't be reached.", "connection": "The server can't be reached.",
"forbidden": "You don't have permissions to access this.", "forbidden": "You don't have permissions to access this.",
@ -130,17 +133,17 @@
"archive": "Archive", "archive": "Archive",
"archiveMessage": "Choose archive name and format:", "archiveMessage": "Choose archive name and format:",
"copy": "Copy", "copy": "Copy",
"copyMessage": "Choose the place to copy your files:", "copyMessage": "Choose the location to copy your files to:",
"currentlyNavigating": "Currently navigating on:", "currentlyNavigating": "Currently navigating on:",
"deleteMessageMultiple": "Are you sure you want to delete {count} file(s)?", "deleteMessageMultiple": "Are you sure you wish to delete {count} file(s)?",
"deleteMessageShare": "Are you sure you want to delete this share({path})?", "deleteMessageSingle": "Are you sure you wish to delete this file/folder?",
"deleteMessageSingle": "Are you sure you want to delete this file/folder?", "deleteMessageShare": "Are you sure you wish to delete this share({path})?",
"deleteTitle": "Delete files", "deleteTitle": "Delete files",
"directories": "Directories", "directories": "Directories",
"directoriesAndFiles": "Directories and files", "directoriesAndFiles": "Directories and files",
"displayName": "Display Name:", "displayName": "Display Name:",
"download": "Download files", "download": "Download files",
"downloadMessage": "Choose the format you want to download.", "downloadMessage": "Choose the format you wish to download.",
"error": "Something went wrong", "error": "Something went wrong",
"execute": "Execute", "execute": "Execute",
"fileInfo": "File information", "fileInfo": "File information",
@ -150,15 +153,14 @@
"inodeCount": "({count} inodes)", "inodeCount": "({count} inodes)",
"lastModified": "Last Modified", "lastModified": "Last Modified",
"move": "Move", "move": "Move",
"moveMessage": "Choose new house for your file(s)/folder(s):", "moveMessage": "Choose new home for your file(s)/folder(s):",
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.", "newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
"newDir": "New directory", "newDir": "New directory",
"newDirMessage": "Write the name of the new directory.", "newDirMessage": "Name your new directory.",
"newFile": "New file", "newFile": "New file",
"newFileMessage": "Write the name of the new file.", "newFileMessage": "Name your new file.",
"numberDirs": "Number of directories", "numberDirs": "Number of directories",
"numberFiles": "Number of files", "numberFiles": "Number of files",
"optionalPassword": "Optional password",
"others": "Others", "others": "Others",
"owner": "Owner", "owner": "Owner",
"permissions": "Permissions", "permissions": "Permissions",
@ -167,7 +169,7 @@
"rename": "Rename", "rename": "Rename",
"renameMessage": "Insert a new name for", "renameMessage": "Insert a new name for",
"replace": "Replace", "replace": "Replace",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to continue to upload or replace the existing one?\n", "replaceMessage": "One of the files you're trying to upload has a conflicting name. Do you wish to skip this file and continue to upload or replace the existing one?\n",
"schedule": "Schedule", "schedule": "Schedule",
"scheduleMessage": "Pick a date and time to schedule the publication of this post.", "scheduleMessage": "Pick a date and time to schedule the publication of this post.",
"show": "Show", "show": "Show",
@ -184,6 +186,8 @@
"uploadFiles": "Uploading {files} files...", "uploadFiles": "Uploading {files} files...",
"uploadFolder": "Folder", "uploadFolder": "Folder",
"uploadMessage": "Select an option to upload.", "uploadMessage": "Select an option to upload.",
"optionalPassword": "Optional password",
"resolution": "Resolution",
"write": "Write" "write": "Write"
}, },
"search": { "search": {
@ -215,20 +219,20 @@
"createUserDir": "Auto create user home dir while adding new user", "createUserDir": "Auto create user home dir while adding new user",
"tusUploads": "Chunked Uploads", "tusUploads": "Chunked Uploads",
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.", "tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting a bytes input or a string like 10MB, 1GB etc.", "tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.",
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.", "tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
"userHomeBasePath": "Base path for user home directories", "userHomeBasePath": "Base path for user home directories",
"userScopeGenerationPlaceholder": "The scope will be auto generated", "userScopeGenerationPlaceholder": "The scope will be auto generated",
"createUserHomeDirectory": "Create user home directory", "createUserHomeDirectory": "Create user home directory",
"customStylesheet": "Custom Stylesheet", "customStylesheet": "Custom Stylesheet",
"defaultUserDescription": "This are the default settings for new users.", "defaultUserDescription": "These are the default settings for new users.",
"disableExternalLinks": "Disable external links (except documentation)", "disableExternalLinks": "Disable external links (except documentation)",
"disableUsedDiskPercentage": "Disable used disk percentage graph", "disableUsedDiskPercentage": "Disable used disk percentage graph",
"documentation": "documentation", "documentation": "documentation",
"examples": "Examples", "examples": "Examples",
"executeOnShell": "Execute on shell", "executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.", "executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you wish to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This applies to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.", "globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override these ones.",
"globalSettings": "Global Settings", "globalSettings": "Global Settings",
"hideDotfiles": "Hide dotfiles", "hideDotfiles": "Hide dotfiles",
"insertPath": "Insert the path", "insertPath": "Insert the path",
@ -254,7 +258,7 @@
"permissions": "Permissions", "permissions": "Permissions",
"permissionsHelp": "You can set the user to be an administrator or choose the permissions individually. If you select \"Administrator\", all of the other options will be automatically checked. The management of users remains a privilege of an administrator.\n", "permissionsHelp": "You can set the user to be an administrator or choose the permissions individually. If you select \"Administrator\", all of the other options will be automatically checked. The management of users remains a privilege of an administrator.\n",
"profileSettings": "Profile Settings", "profileSettings": "Profile Settings",
"ruleExample1": "prevents the access to any dot file (such as .git, .gitignore) in every folder.\n", "ruleExample1": "prevents the access to any dotfile (such as .git, .gitignore) in every folder.\n",
"ruleExample2": "blocks the access to the file named Caddyfile on the root of the scope.", "ruleExample2": "blocks the access to the file named Caddyfile on the root of the scope.",
"rules": "Rules", "rules": "Rules",
"rulesHelp": "Here you can define a set of allow and disallow rules for this specific user. The blocked files won't show up in the listings and they wont be accessible to the user. We support regex and paths relative to the users scope.\n", "rulesHelp": "Here you can define a set of allow and disallow rules for this specific user. The blocked files won't show up in the listings and they wont be accessible to the user. We support regex and paths relative to the users scope.\n",

View File

@ -7,7 +7,7 @@ import i18n from "@/i18n";
import Vue from "@/utils/vue"; import Vue from "@/utils/vue";
import { recaptcha, loginPage } from "@/utils/constants"; import { recaptcha, loginPage } from "@/utils/constants";
import { login, validateLogin } from "@/utils/auth"; import { login, validateLogin } from "@/utils/auth";
import App from "@/App"; import App from "@/App.vue";
cssVars(); cssVars();

View File

@ -1,16 +1,16 @@
import Vue from "vue"; import Vue from "vue";
import Router from "vue-router"; import Router from "vue-router";
import Login from "@/views/Login"; import Login from "@/views/Login.vue";
import Layout from "@/views/Layout"; import Layout from "@/views/Layout.vue";
import Files from "@/views/Files"; import Files from "@/views/Files.vue";
import Share from "@/views/Share"; import Share from "@/views/Share.vue";
import Users from "@/views/settings/Users"; import Users from "@/views/settings/Users.vue";
import User from "@/views/settings/User"; import User from "@/views/settings/User.vue";
import Settings from "@/views/Settings"; import Settings from "@/views/Settings.vue";
import GlobalSettings from "@/views/settings/Global"; import GlobalSettings from "@/views/settings/Global.vue";
import ProfileSettings from "@/views/settings/Profile"; import ProfileSettings from "@/views/settings/Profile.vue";
import Shares from "@/views/settings/Shares"; import Shares from "@/views/settings/Shares.vue";
import Errors from "@/views/Errors"; import Errors from "@/views/Errors.vue";
import store from "@/store"; import store from "@/store";
import { baseURL, name } from "@/utils/constants"; import { baseURL, name } from "@/utils/constants";
import i18n, { rtlLanguages } from "@/i18n"; import i18n, { rtlLanguages } from "@/i18n";
@ -33,7 +33,7 @@ const titles = {
}; };
const router = new Router({ const router = new Router({
base: baseURL, base: import.meta.env.PROD ? baseURL : "",
mode: "history", mode: "history",
routes: [ routes: [
{ {

View File

@ -6,7 +6,7 @@ const getters = {
getters.isListing && state.contextMenu !== null, getters.isListing && state.contextMenu !== null,
selectedCount: (state) => state.selected.length, selectedCount: (state) => state.selected.length,
progress: (state) => { progress: (state) => {
if (state.upload.progress.length == 0) { if (state.upload.progress.length === 0) {
return 0; return 0;
} }
@ -16,9 +16,7 @@ const getters = {
return Math.ceil((sum / totalSize) * 100); return Math.ceil((sum / totalSize) * 100);
}, },
filesInUploadCount: (state) => { filesInUploadCount: (state) => {
let total = return Object.keys(state.upload.uploads).length + state.upload.queue.length;
Object.keys(state.upload.uploads).length + state.upload.queue.length;
return total;
}, },
filesInUpload: (state) => { filesInUpload: (state) => {
let files = []; let files = [];
@ -59,6 +57,16 @@ const getters = {
} }
return true; return true;
}, },
currentPrompt: (state) => {
return state.prompts.length > 0
? state.prompts[state.prompts.length - 1]
: null;
},
currentPromptName: (_, getters) => {
return getters.currentPrompt?.prompt;
},
uploadSpeed: (state) => state.upload.speedMbyte,
eta: (state) => state.upload.eta,
}; };
export default getters; export default getters;

View File

@ -21,12 +21,10 @@ const state = {
reload: false, reload: false,
selected: [], selected: [],
multiple: false, multiple: false,
show: null, prompts: [],
showShell: false, showShell: false,
showConfirm: null,
contextMenu: null, contextMenu: null,
diskUsages: {}, diskUsages: {},
showAction: null,
}; };
export default new Vuex.Store({ export default new Vuex.Store({

View File

@ -11,6 +11,8 @@ const state = {
progress: [], progress: [],
queue: [], queue: [],
uploads: {}, uploads: {},
speedMbyte: 0,
eta: 0,
}; };
const mutations = { const mutations = {

View File

@ -3,30 +3,35 @@ import moment from "moment";
const mutations = { const mutations = {
closeHovers: (state) => { closeHovers: (state) => {
state.show = null; state.prompts.pop();
state.showConfirm = null;
state.showAction = null;
}, },
toggleShell: (state) => { toggleShell: (state) => {
state.show = null;
state.showShell = !state.showShell; state.showShell = !state.showShell;
}, },
showHover: (state, value) => { showHover: (state, value) => {
if (typeof value !== "object") { if (typeof value !== "object") {
state.show = value; state.prompts.push({
prompt: value,
confirm: null,
action: null,
props: null,
});
return; return;
} }
state.show = value.prompt; state.prompts.push({
state.showConfirm = value.confirm; prompt: value.prompt, // Should not be null
if (value.action !== undefined) { confirm: value?.confirm,
state.showAction = value.action; action: value?.action,
} props: value?.props,
});
}, },
showError: (state) => { showError: (state) => {
state.show = "error"; state.prompts.push("error");
}, },
showSuccess: (state) => { showSuccess: (state) => {
state.show = "success"; state.prompts.push("success");
}, },
setLoading: (state, value) => { setLoading: (state, value) => {
state.loading = value; state.loading = value;
@ -74,8 +79,15 @@ const mutations = {
} }
}, },
updateRequest: (state, value) => { updateRequest: (state, value) => {
const selectedItems = state.selected.map((i) => state.req.items[i]);
state.oldReq = state.req; state.oldReq = state.req;
state.req = value; state.req = value;
state.selected = [];
if (!state.req?.items) return;
state.selected = state.req.items
.filter((item) => selectedItems.some((rItem) => rItem.url === item.url))
.map((item) => item.index);
}, },
updateClipboard: (state, value) => { updateClipboard: (state, value) => {
state.clipboard.key = value.key; state.clipboard.key = value.key;
@ -105,6 +117,21 @@ const mutations = {
tmp[value.path] = value.usage; tmp[value.path] = value.usage;
state.diskUsages = tmp; state.diskUsages = tmp;
}, },
setUploadSpeed: (state, value) => {
state.upload.speedMbyte = value;
},
setETA(state, value) {
state.upload.eta = value;
},
resetUpload(state) {
state.upload.uploads = {};
state.upload.queue = [];
state.upload.progress = [];
state.upload.sizes = [];
state.upload.id = 0;
state.upload.speedMbyte = 0;
state.upload.eta = 0;
},
}; };
export default mutations; export default mutations;

View File

@ -25,7 +25,7 @@ export async function validateLogin() {
await renew(localStorage.getItem("jwt")); await renew(localStorage.getItem("jwt"));
} }
} catch (_) { } catch (_) {
console.warn('Invalid JWT token in storage') // eslint-disable-line console.warn("Invalid JWT token in storage"); // eslint-disable-line
} }
} }

View File

@ -0,0 +1,6 @@
import { partial } from "filesize";
/**
* Formats filesize as KiB/MiB/...
*/
export const filesize = partial({ base: 2 });

View File

@ -10,7 +10,7 @@
</template> </template>
<script> <script>
import HeaderBar from "@/components/header/HeaderBar"; import HeaderBar from "@/components/header/HeaderBar.vue";
const errors = { const errors = {
0: { 0: {

View File

@ -27,11 +27,11 @@
import { files as api } from "@/api"; import { files as api } from "@/api";
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations } from "vuex";
import HeaderBar from "@/components/header/HeaderBar"; import HeaderBar from "@/components/header/HeaderBar.vue";
import Breadcrumbs from "@/components/Breadcrumbs"; import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Errors from "@/views/Errors"; import Errors from "@/views/Errors.vue";
import Preview from "@/views/files/Preview"; import Preview from "@/views/files/Preview.vue";
import Listing from "@/views/files/Listing"; import Listing from "@/views/files/Listing.vue";
function clean(path) { function clean(path) {
return path.endsWith("/") ? path.slice(0, -1) : path; return path.endsWith("/") ? path.slice(0, -1) : path;
@ -45,7 +45,7 @@ export default {
Errors, Errors,
Preview, Preview,
Listing, Listing,
Editor: () => import("@/views/files/Editor"), Editor: () => import("@/views/files/Editor.vue"),
}, },
data: function () { data: function () {
return { return {
@ -55,7 +55,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(["req", "reload", "loading", "show"]), ...mapState(["req", "reload", "loading"]),
currentView() { currentView() {
if (this.req.type == undefined) { if (this.req.type == undefined) {
return null; return null;

View File

@ -17,11 +17,11 @@
<script> <script>
import { mapState, mapGetters } from "vuex"; import { mapState, mapGetters } from "vuex";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar.vue";
import Prompts from "@/components/prompts/Prompts"; import Prompts from "@/components/prompts/Prompts.vue";
import ContextMenu from "@/components/files/ContextMenu"; import ContextMenu from "@/components/files/ContextMenu.vue";
import Shell from "@/components/Shell"; import Shell from "@/components/Shell.vue";
import UploadFiles from "../components/prompts/UploadFiles"; import UploadFiles from "../components/prompts/UploadFiles.vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
export default { export default {
@ -34,7 +34,7 @@ export default {
UploadFiles, UploadFiles,
}, },
computed: { computed: {
...mapGetters(["isLogged", "isVisibleContext", "progress"]), ...mapGetters(["isLogged", "progress", "currentPrompt", "isVisibleContext"]),
...mapState(["user"]), ...mapState(["user"]),
isExecEnabled: () => enableExec, isExecEnabled: () => enableExec,
}, },
@ -43,7 +43,7 @@ export default {
this.$store.commit("hideContextMenu"); this.$store.commit("hideContextMenu");
this.$store.commit("resetSelected"); this.$store.commit("resetSelected");
this.$store.commit("multiple", false); this.$store.commit("multiple", false);
if (this.$store.state.show !== "success") if (this.currentPrompt?.prompt !== "success")
this.$store.commit("closeHovers"); this.$store.commit("closeHovers");
}, },
}, },

View File

@ -52,7 +52,7 @@
<script> <script>
import { mapState } from "vuex"; import { mapState } from "vuex";
import HeaderBar from "@/components/header/HeaderBar"; import HeaderBar from "@/components/header/HeaderBar.vue";
export default { export default {
name: "settings", name: "settings",

View File

@ -182,15 +182,15 @@
<script> <script>
import { mapState, mapMutations, mapGetters } from "vuex"; import { mapState, mapMutations, mapGetters } from "vuex";
import { pub as api } from "@/api"; import { pub as api } from "@/api";
import { filesize } from "filesize"; import { filesize } from "@/utils";
import moment from "moment"; import moment from "moment";
import HeaderBar from "@/components/header/HeaderBar"; import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action"; import Action from "@/components/header/Action.vue";
import Breadcrumbs from "@/components/Breadcrumbs"; import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Errors from "@/views/Errors"; import Errors from "@/views/Errors.vue";
import QrcodeVue from "qrcode.vue"; import QrcodeVue from "qrcode.vue";
import Item from "@/components/files/ListingItem"; import Item from "@/components/files/ListingItem.vue";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
export default { export default {

View File

@ -26,13 +26,13 @@ import { theme } from "@/utils/constants";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import url from "@/utils/url"; import url from "@/utils/url";
import { version as ace_version } from "ace-builds";
import ace from "ace-builds/src-min-noconflict/ace.js"; import ace from "ace-builds/src-min-noconflict/ace.js";
import modelist from "ace-builds/src-min-noconflict/ext-modelist.js"; import modelist from "ace-builds/src-min-noconflict/ext-modelist.js";
import "ace-builds/webpack-resolver";
import HeaderBar from "@/components/header/HeaderBar"; import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action"; import Action from "@/components/header/Action.vue";
import Breadcrumbs from "@/components/Breadcrumbs"; import Breadcrumbs from "@/components/Breadcrumbs.vue";
export default { export default {
name: "editor", name: "editor",
@ -95,6 +95,11 @@ export default {
mounted: function () { mounted: function () {
const fileContent = this.req.content || ""; const fileContent = this.req.content || "";
ace.config.set(
"basePath",
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
);
this.editor = ace.edit("editor", { this.editor = ace.edit("editor", {
value: fileContent, value: fileContent,
showPrintMargin: false, showPrintMargin: false,

View File

@ -1,7 +1,8 @@
<template> <template>
<div> <div>
<header-bar showMenu showLogo> <header-bar showMenu showLogo>
<search /> <title /> <search />
<title />
<action <action
class="search-button" class="search-button"
icon="search" icon="search"
@ -308,10 +309,10 @@ import css from "@/utils/css";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import HeaderBar from "@/components/header/HeaderBar"; import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action"; import Action from "@/components/header/Action.vue";
import Search from "@/components/Search"; import Search from "@/components/Search.vue";
import Item from "@/components/files/ListingItem"; import Item from "@/components/files/ListingItem.vue";
export default { export default {
name: "listing", name: "listing",
@ -331,16 +332,8 @@ export default {
}; };
}, },
computed: { computed: {
...mapState([ ...mapState(["req", "selected", "user", "multiple", "selected", "loading"]),
"req", ...mapGetters(["selectedCount", "currentPrompt", "onlyArchivesSelected"]),
"selected",
"user",
"show",
"multiple",
"selected",
"loading",
]),
...mapGetters(["selectedCount", "onlyArchivesSelected"]),
nameSorted() { nameSorted() {
return this.req.sorting.by === "name"; return this.req.sorting.by === "name";
}, },
@ -483,7 +476,7 @@ export default {
}, },
keyEvent(event) { keyEvent(event) {
// No prompts are shown // No prompts are shown
if (this.show !== null) { if (this.currentPrompt !== null) {
return; return;
} }

View File

@ -143,14 +143,14 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapGetters, mapState } from "vuex";
import { files as api } from "@/api"; import { files as api } from "@/api";
import { resizePreview } from "@/utils/constants"; import { resizePreview } from "@/utils/constants";
import url from "@/utils/url"; import url from "@/utils/url";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import HeaderBar from "@/components/header/HeaderBar"; import HeaderBar from "@/components/header/HeaderBar.vue";
import Action from "@/components/header/Action"; import Action from "@/components/header/Action.vue";
import ExtendedImage from "@/components/files/ExtendedImage"; import ExtendedImage from "@/components/files/ExtendedImage.vue";
const mediaTypes = ["image", "video", "audio", "blob"]; const mediaTypes = ["image", "video", "audio", "blob"];
@ -177,7 +177,8 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(["req", "user", "oldReq", "jwt", "loading", "show"]), ...mapState(["req", "user", "oldReq", "jwt", "loading"]),
...mapGetters(["currentPrompt"]),
hasPrevious() { hasPrevious() {
return this.previousLink !== ""; return this.previousLink !== "";
}, },
@ -195,7 +196,7 @@ export default {
return api.getDownloadURL(this.req, true); return api.getDownloadURL(this.req, true);
}, },
showMore() { showMore() {
return this.$store.state.show === "more"; return this.currentPrompt?.prompt === "more";
}, },
isResizeEnabled() { isResizeEnabled() {
return resizePreview; return resizePreview;
@ -247,7 +248,7 @@ export default {
this.$router.replace({ path: this.nextLink }); this.$router.replace({ path: this.nextLink });
}, },
key(event) { key(event) {
if (this.show !== null) { if (this.currentPrompt !== null) {
return; return;
} }

View File

@ -223,10 +223,10 @@
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations } from "vuex";
import { settings as api } from "@/api"; import { settings as api } from "@/api";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
import UserForm from "@/components/settings/UserForm"; import UserForm from "@/components/settings/UserForm.vue";
import Rules from "@/components/settings/Rules"; import Rules from "@/components/settings/Rules.vue";
import Themes from "@/components/settings/Themes"; import Themes from "@/components/settings/Themes.vue";
import Errors from "@/views/Errors"; import Errors from "@/views/Errors.vue";
export default { export default {
name: "settings", name: "settings",

View File

@ -74,7 +74,7 @@
<script> <script>
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations } from "vuex";
import { users as api } from "@/api"; import { users as api } from "@/api";
import Languages from "@/components/settings/Languages"; import Languages from "@/components/settings/Languages.vue";
import i18n, { rtlLanguages } from "@/i18n"; import i18n, { rtlLanguages } from "@/i18n";
export default { export default {

View File

@ -65,7 +65,7 @@ import { share as api, users } from "@/api";
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations } from "vuex";
import moment from "moment"; import moment from "moment";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import Errors from "@/views/Errors"; import Errors from "@/views/Errors.vue";
export default { export default {
name: "shares", name: "shares",

View File

@ -37,7 +37,7 @@
</form> </form>
</div> </div>
<div v-if="$store.state.show === 'deleteUser'" class="card floating"> <div v-if="this.currentPromptName === 'deleteUser'" class="card floating">
<div class="card-content"> <div class="card-content">
<p>Are you sure you want to delete this user?</p> <p>Are you sure you want to delete this user?</p>
</div> </div>
@ -61,10 +61,10 @@
</template> </template>
<script> <script>
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations, mapGetters } from "vuex";
import { users as api, settings } from "@/api"; import { users as api, settings } from "@/api";
import UserForm from "@/components/settings/UserForm"; import UserForm from "@/components/settings/UserForm.vue";
import Errors from "@/views/Errors"; import Errors from "@/views/Errors.vue";
import deepClone from "lodash.clonedeep"; import deepClone from "lodash.clonedeep";
export default { export default {
@ -89,6 +89,7 @@ export default {
return this.$route.path === "/settings/users/new"; return this.$route.path === "/settings/users/new";
}, },
...mapState(["loading"]), ...mapState(["loading"]),
...mapGetters(["currentPrompt", "currentPromptName"]),
}, },
watch: { watch: {
$route: "fetchData", $route: "fetchData",
@ -109,7 +110,7 @@ export default {
this.user = { this.user = {
...defaults, ...defaults,
username: "", username: "",
passsword: "", password: "",
rules: [], rules: [],
lockPassword: false, lockPassword: false,
id: 0, id: 0,

View File

@ -44,7 +44,7 @@
<script> <script>
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations } from "vuex";
import { users as api } from "@/api"; import { users as api } from "@/api";
import Errors from "@/views/Errors"; import Errors from "@/views/Errors.vue";
export default { export default {
name: "users", name: "users",

70
frontend/vite.config.js Normal file
View File

@ -0,0 +1,70 @@
import { fileURLToPath, URL } from "node:url";
import path from "node:path";
import { defineConfig } from "vite";
import legacy from "@vitejs/plugin-legacy";
import vue2 from "@vitejs/plugin-vue2";
import { compression } from "vite-plugin-compression2";
import pluginRewriteAll from "vite-plugin-rewrite-all";
const plugins = [
vue2(),
legacy({
targets: ["ie >= 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"],
}),
compression({ include: /\.js$/i, deleteOriginalAssets: true }),
pluginRewriteAll(), // fixes 404 error with paths containing dot in dev server
];
const resolve = {
alias: {
vue: "vue/dist/vue.esm.js",
"@/": `${path.resolve(__dirname, "src")}/`,
},
};
// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
if (command === "serve") {
return {
plugins,
resolve,
server: {
proxy: {
"/api/command": {
target: "ws://127.0.0.1:8080",
ws: true,
},
"/api": "http://127.0.0.1:8080",
},
},
};
} else {
// command === 'build'
return {
plugins,
resolve,
base: "",
build: {
rollupOptions: {
input: {
index: fileURLToPath(
new URL(`./public/index.html`, import.meta.url)
),
},
},
},
experimental: {
renderBuiltUrl(filename, { hostType }) {
if (hostType === "js") {
return { runtime: `window.__prependStaticUrl("${filename}")` };
} else if (hostType === "html") {
return `[{[ .StaticURL ]}]/${filename}`;
} else {
return { relative: true };
}
},
},
};
}
});

View File

@ -1,15 +0,0 @@
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
runtimeCompiler: true,
publicPath: "[{[ .StaticURL ]}]",
parallel: 2,
configureWebpack: {
plugins: [
new CompressionPlugin({
include: /\.js$/,
deleteOriginalAssets: true,
}),
],
},
};

10
go.mod
View File

@ -23,9 +23,9 @@ require (
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
go.etcd.io/bbolt v1.3.7 go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.10.0 golang.org/x/crypto v0.14.0
golang.org/x/image v0.5.0 golang.org/x/image v0.10.0
golang.org/x/text v0.10.0 golang.org/x/text v0.13.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@ -59,8 +59,8 @@ require (
github.com/ulikunitz/xz v0.5.9 // indirect github.com/ulikunitz/xz v0.5.9 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/net v0.11.0 // indirect golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.9.0 // indirect golang.org/x/sys v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect

27
go.sum
View File

@ -292,8 +292,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -307,8 +307,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -331,6 +331,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -365,8 +366,9 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -387,6 +389,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -428,10 +431,12 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -440,8 +445,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -493,6 +499,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
PORT=$(jq .port /.filebrowser.json) PORT=${FB_PORT:-$(jq .port /.filebrowser.json)}
curl -f http://localhost:$PORT/health || exit 1 curl -f http://localhost:$PORT/health || exit 1

View File

@ -16,7 +16,7 @@ import (
) )
const ( const (
TokenExpirationTime = time.Hour * 2 DefaultTokenExpirationTime = time.Hour * 2
) )
type userInfo struct { type userInfo struct {
@ -101,19 +101,21 @@ func withAdmin(fn handleFunc) handleFunc {
}) })
} }
var loginHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { func loginHandler(tokenExpireTime time.Duration) handleFunc {
auther, err := d.store.Auth.Get(d.settings.AuthMethod) return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if err != nil { auther, err := d.store.Auth.Get(d.settings.AuthMethod)
return http.StatusInternalServerError, err if err != nil {
} return http.StatusInternalServerError, err
}
user, err := auther.Auth(r, d.store.Users, d.settings, d.server) user, err := auther.Auth(r, d.store.Users, d.settings, d.server)
if err == os.ErrPermission { if err == os.ErrPermission {
return http.StatusForbidden, nil return http.StatusForbidden, nil
} else if err != nil { } else if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} else { } else {
return printToken(w, r, d, user) return printToken(w, r, d, user, tokenExpireTime)
}
} }
} }
@ -172,11 +174,14 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
return http.StatusOK, nil return http.StatusOK, nil
} }
var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { func renewHandler(tokenExpireTime time.Duration) handleFunc {
return printToken(w, r, d, d.user) return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
}) w.Header().Set("X-Renew-Token", "false")
return printToken(w, r, d, d.user, tokenExpireTime)
})
}
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) { func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User, tokenExpirationTime time.Duration) (int, error) {
claims := &authToken{ claims := &authToken{
User: userInfo{ User: userInfo{
ID: user.ID, ID: user.ID,
@ -191,7 +196,7 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use
}, },
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()), IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpirationTime)), ExpiresAt: jwt.NewNumericDate(time.Now().Add(tokenExpirationTime)),
Issuer: "File Browser", Issuer: "File Browser",
}, },
} }

View File

@ -50,7 +50,9 @@ func (d *data) Check(path string) bool {
func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler { func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") for k, v := range globalHeaders {
w.Header().Set(k, v)
}
settings, err := store.Settings.Get() settings, err := store.Settings.Get()
if err != nil { if err != nil {

9
http/headers.go Normal file
View File

@ -0,0 +1,9 @@
//go:build !dev
// +build !dev
package http
// global headers to append to every response
var globalHeaders = map[string]string{
"Cache-Control": "no-cache, no-store, must-revalidate",
}

15
http/headers_dev.go Normal file
View File

@ -0,0 +1,15 @@
//go:build dev
// +build dev
package http
// global headers to append to every response
// cross-origin headers are necessary to be able to
// access them from a different URL during development
var globalHeaders = map[string]string{
"Cache-Control": "no-cache, no-store, must-revalidate",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Credentials": "true",
}

View File

@ -48,9 +48,10 @@ func NewHandler(
api := r.PathPrefix("/api").Subrouter() api := r.PathPrefix("/api").Subrouter()
api.Handle("/login", monkey(loginHandler, "")) tokenExpirationTime := server.GetTokenExpirationTime(DefaultTokenExpirationTime)
api.Handle("/login", monkey(loginHandler(tokenExpirationTime), ""))
api.Handle("/signup", monkey(signupHandler, "")) api.Handle("/signup", monkey(signupHandler, ""))
api.Handle("/renew", monkey(renewHandler, "")) api.Handle("/renew", monkey(renewHandler(tokenExpirationTime), ""))
users := api.PathPrefix("/users").Subrouter() users := api.PathPrefix("/users").Subrouter()
users.Handle("", monkey(usersGetHandler, "")).Methods("GET") users.Handle("", monkey(usersGetHandler, "")).Methods("GET")
@ -66,11 +67,9 @@ func NewHandler(
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler(fileCache), "/api/resources")).Methods("PATCH") api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler(fileCache), "/api/resources")).Methods("PATCH")
api.PathPrefix("/tus").Handler(monkey(tusPostHandler(), "/api/tus")).Methods("POST") api.PathPrefix("/tus").Handler(monkey(tusPostHandler(), "/api/tus")).Methods("POST")
api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD") api.PathPrefix("/tus").Handler(monkey(tusHeadHandler(), "/api/tus")).Methods("HEAD", "GET")
api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH") api.PathPrefix("/tus").Handler(monkey(tusPatchHandler(), "/api/tus")).Methods("PATCH")
// api.PathPrefix("/usage").Handler(monkey(diskUsage, "/api/usage")).Methods("GET")
api.Path("/shares").Handler(monkey(shareListHandler, "/api/shares")).Methods("GET") api.Path("/shares").Handler(monkey(shareListHandler, "/api/shares")).Methods("GET")
api.PathPrefix("/share").Handler(monkey(shareGetsHandler, "/api/share")).Methods("GET") api.PathPrefix("/share").Handler(monkey(shareGetsHandler, "/api/share")).Methods("GET")
api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST") api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST")

View File

@ -476,6 +476,7 @@ type DiskUsageResponse struct {
} }
//lint:ignore U1000 unused in this fork //lint:ignore U1000 unused in this fork
//nolint:deadcode
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file, err := files.NewFileInfo(files.FileOptions{ file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs, Fs: d.user.Fs,
@ -664,7 +665,7 @@ func chmodActionHandler(r *http.Request, d *data) error {
return errors.ErrPermissionDenied return errors.ErrPermissionDenied
} }
mode, err := strconv.ParseUint(perms, 10, 32) //nolint:gomnd mode, err := strconv.ParseUint(perms, 10, 32)
if err != nil { if err != nil {
return errors.ErrInvalidRequestParams return errors.ErrInvalidRequestParams
} }

View File

@ -109,7 +109,7 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs
} }
w.Header().Set("x-xss-protection", "1; mode=block") w.Header().Set("x-xss-protection", "1; mode=block")
return handleWithStaticData(w, r, d, assetsFs, "index.html", "text/html; charset=utf-8") return handleWithStaticData(w, r, d, assetsFs, "public/index.html", "text/html; charset=utf-8")
}, "", store, server) }, "", store, server)
static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
@ -117,6 +117,10 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs
return http.StatusNotFound, nil return http.StatusNotFound, nil
} }
if strings.HasSuffix(r.URL.Path, "/") {
return http.StatusNotFound, nil
}
const maxAge = 86400 // 1 day const maxAge = 86400 // 1 day
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%v", maxAge)) w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%v", maxAge))

View File

@ -40,7 +40,7 @@ func tusPostHandler() handleFunc {
return errToStatus(err), err return errToStatus(err), err
} }
fileFlags := os.O_CREATE fileFlags := os.O_CREATE | os.O_WRONLY
if r.URL.Query().Get("override") == "true" { if r.URL.Query().Get("override") == "true" {
fileFlags |= os.O_TRUNC fileFlags |= os.O_TRUNC
} }

View File

@ -2,7 +2,9 @@ package settings
import ( import (
"crypto/rand" "crypto/rand"
"log"
"strings" "strings"
"time"
"github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/rules"
) )
@ -49,6 +51,7 @@ type Server struct {
TypeDetectionByHeader bool `json:"typeDetectionByHeader"` TypeDetectionByHeader bool `json:"typeDetectionByHeader"`
AuthHook string `json:"authHook"` AuthHook string `json:"authHook"`
HiddenFiles map[string]struct{} `json:"hiddenFiles"` HiddenFiles map[string]struct{} `json:"hiddenFiles"`
TokenExpirationTime string `json:"tokenExpirationTime"`
} }
// Clean cleans any variables that might need cleaning. // Clean cleans any variables that might need cleaning.
@ -56,6 +59,19 @@ func (s *Server) Clean() {
s.BaseURL = strings.TrimSuffix(s.BaseURL, "/") s.BaseURL = strings.TrimSuffix(s.BaseURL, "/")
} }
func (s *Server) GetTokenExpirationTime(fallback time.Duration) time.Duration {
if s.TokenExpirationTime == "" {
return fallback
}
if duration, err := time.ParseDuration(s.TokenExpirationTime); err == nil {
return duration
} else {
log.Printf("[WARN] Failed to parse tokenExpirationTime: %v", err)
return fallback
}
}
// GenerateKey generates a key of 512 bits. // GenerateKey generates a key of 512 bits.
func GenerateKey() ([]byte, error) { func GenerateKey() ([]byte, error) {
b := make([]byte, 64) //nolint:gomnd b := make([]byte, 64) //nolint:gomnd