Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
018be705a6 | ||
|
|
ff5010ed4d | ||
|
|
226038b11d | ||
|
|
206180de0d | ||
|
|
5b0f388fa5 |
No files matched your search
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: frontend/dist
|
||||
path: backend/http/dist
|
||||
build_backend:
|
||||
name: Build backend
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -2,6 +2,60 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v0.8.3-beta
|
||||
|
||||
**BugFixes**:
|
||||
- fixed search bar style bug in mobile #1147
|
||||
|
||||
## v0.8.2-beta
|
||||
|
||||
**New Features**:
|
||||
- added `source.config.denyByDefault` configuration to enable a deny-by-default access rule. A source enabled with this will deny access unless an "allow" rule was specifically created. (Similar to creating a root-level denyAll rule)
|
||||
- allow oidc user source access and permission based on username and groups is fulfilled by denyByDefault source with access rules https://github.com/gtsteffaniak/filebrowser/issues/824
|
||||
- "open parent folder" in context menu and search results https://github.com/gtsteffaniak/filebrowser/issues/1121
|
||||
- added friendly "share not found" page.
|
||||
|
||||
**Notes**:
|
||||
- 8.0 ffmpeg version bundled with docker
|
||||
- go 1.25 upgrade with green tea GC enabled
|
||||
- totp secrets accept non-secure strings, only throwing warning
|
||||
- adjusted download limit so it also counts viewing text "content" of files (like in editor). You can also "disable file viewing" to stop the editor from showing. lower quality file image previews are not counted as downloads.
|
||||
- updated invalid share message to be more clear https://github.com/gtsteffaniak/filebrowser/issues/1120
|
||||
|
||||
**BugFixes**:
|
||||
- fixed /public/static routes issue
|
||||
- shares redirect to login - https://github.com/gtsteffaniak/filebrowser/issues/1109
|
||||
- some static assets not available to anonymous user - https://github.com/gtsteffaniak/filebrowser/issues/1102
|
||||
- more safari style issues https://github.com/gtsteffaniak/filebrowser/issues/1110
|
||||
- fix public share download issues https://github.com/gtsteffaniak/filebrowser/issues/1118 https://github.com/gtsteffaniak/filebrowser/issues/1089
|
||||
- fixed disable file viewer setting and enforced on backend
|
||||
|
||||
## v0.8.1-beta
|
||||
|
||||
**New Features**:
|
||||
- api for generate download link (see swagger) https://github.com/gtsteffaniak/filebrowser/issues/1007
|
||||
- added `source.config.disabled` option to disable a source without removing it from config file.
|
||||
- added `source.config.private` option to designate as private -- currently just means no sharing permitted.
|
||||
- hide share card in share
|
||||
- download count for a share shows up on share management
|
||||
|
||||
**Notes**:
|
||||
- updated description for indexingIntervalMinutes https://github.com/gtsteffaniak/filebrowser/issues/1067
|
||||
|
||||
**BugFixes**:
|
||||
- fixed styling issues https://github.com/gtsteffaniak/filebrowser/issues/1086 https://github.com/gtsteffaniak/filebrowser/issues/1081 https://github.com/gtsteffaniak/filebrowser/issues/1082 https://github.com/gtsteffaniak/filebrowser/issues/1098
|
||||
- fix download limit issue https://github.com/gtsteffaniak/filebrowser/issues/1085
|
||||
- fixed oidc user defaults for new user https://github.com/gtsteffaniak/filebrowser/issues/1071
|
||||
- shares get updated when files moved in ui https://github.com/gtsteffaniak/filebrowser/issues/760
|
||||
- click listing behavior doesn't clear (introduced in 0.8.0) https://github.com/gtsteffaniak/filebrowser/issues/1101
|
||||
- show download count and limit in share list in settings https://github.com/gtsteffaniak/filebrowser/issues/1103
|
||||
- fix windows alt+arrow movement issue https://github.com/gtsteffaniak/filebrowser/issues/1094
|
||||
- nav memory issue for filenames with brackets https://github.com/gtsteffaniak/filebrowser/issues/1092
|
||||
- files with "+"" in name issue https://github.com/gtsteffaniak/filebrowser/issues/1089
|
||||
- fixed editor bug in share view https://github.com/gtsteffaniak/filebrowser/issues/1084
|
||||
- other share related issues https://github.com/gtsteffaniak/filebrowser/issues/1087 https://github.com/gtsteffaniak/filebrowser/issues/1064
|
||||
|
||||
|
||||
## v0.8.0-beta
|
||||
|
||||
This is a major release, new features and changes could introduce breaking behavior. Here are the known potentially breaking changes:
|
||||
|
||||
+11
-22
@@ -4,7 +4,7 @@ Thank you for your interest in contributing to FileBrowser Quantum! This guide w
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Go 1.24+** (see `backend/go.mod`)
|
||||
- **Go 1.25+** (see `backend/go.mod`)
|
||||
- **Node.js 18.0.0+** with npm 7.0.0+ (see `frontend/package.json`)
|
||||
- **Docker** (optional, for containerized development)
|
||||
- **Git**
|
||||
@@ -31,8 +31,8 @@ cd filebrowser
|
||||
# 2. Initial setup - installs dependencies and creates test config
|
||||
make setup
|
||||
|
||||
# 3. Run in development mode
|
||||
make run
|
||||
# 3. Run in development mode with hot-reloading
|
||||
make dev
|
||||
```
|
||||
|
||||
`make setup` installs all dependencies and creates a test configuration file.
|
||||
@@ -52,14 +52,14 @@ make run
|
||||
- **Framework**: Vue 3 + Vite + TypeScript
|
||||
- **State**: Custom store in `frontend/src/store/`
|
||||
- **API Client**: Axios-based in `frontend/src/api/`
|
||||
- **i18n**: 25+ languages with English as master
|
||||
- **i18n**: 25+ languages with English as main
|
||||
- **Components**: Feature-based organization
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Essential Commands
|
||||
```bash
|
||||
make run # Start development server
|
||||
make dev # Start development server with hot-reloading
|
||||
make test # Run all tests
|
||||
make lint # Check code quality
|
||||
make check-all # Lint + tests
|
||||
@@ -70,12 +70,12 @@ make build # Build Docker image
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
|
||||
Note: consider using make commands above instead.
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev # Dev server with hot reload
|
||||
npm run lint:fix # Auto-fix linting issues
|
||||
npm run typecheck # TypeScript validation
|
||||
npm run i18n:sync # Sync translations
|
||||
npm run i18n:sync # Sync translations changes
|
||||
```
|
||||
|
||||
## Testing
|
||||
@@ -128,12 +128,11 @@ make build-backend # Build Go binary with embedded assets
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
make build # Full image with ffmpeg (~200MB)
|
||||
make run-proxy # Docker Compose with nginx proxy
|
||||
make build # Full image with ffmpeg and muPDF
|
||||
```
|
||||
|
||||
### Configuration
|
||||
- **Test Config**: `backend/test_config.yaml` Auto-generated after running `make setup`
|
||||
- **Test Config**: `backend/test_config.yaml` Auto-generated after running `make setup`
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -141,7 +140,7 @@ make run-proxy # Docker Compose with nginx proxy
|
||||
|
||||
1. Fork and create a feature branch
|
||||
2. Make your changes following the code standards
|
||||
3. Run `make run` to build and run with your changes
|
||||
3. Run `make dev` to build and run with your changes. Supports hot-reloading frontend and backend changes.
|
||||
4. When ready, run `make check-all` to verify tests and linting
|
||||
5. Submit PR with a clear description
|
||||
|
||||
@@ -175,16 +174,6 @@ cd frontend && rm -rf node_modules && npm install
|
||||
cd backend && go mod tidy && go clean -modcache
|
||||
```
|
||||
|
||||
**Debugging:**
|
||||
```bash
|
||||
# Backend with debug logs
|
||||
cd backend
|
||||
FILEBROWSER_NO_EMBEDDED=true go run -tags mupdf . -c test_config.yaml --log-level debug
|
||||
|
||||
# API documentation
|
||||
# Visit /swagger endpoint when running
|
||||
```
|
||||
|
||||
**Authentication Issues:**
|
||||
|
||||
Always first enable debug logging in your config file if you have issues.
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM gtstef/ffmpeg:7.1.1-decode AS ffmpeg
|
||||
FROM gtstef/ffmpeg:8.0-decode AS ffmpeg
|
||||
FROM golang:alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
@@ -6,6 +6,7 @@ WORKDIR /app
|
||||
COPY ./backend ./
|
||||
RUN apk update && apk add --no-cache gcc musl-dev upx
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOEXPERIMENT=greenteagc
|
||||
RUN go build -tags mupdf,musl -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
|
||||
@@ -23,7 +24,6 @@ FROM alpine:latest
|
||||
COPY --from=ffmpeg [ "/ffmpeg", "/ffprobe", "/usr/local/bin/" ]
|
||||
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
|
||||
ENV FILEBROWSER_DATABASE="/home/filebrowser/data/database.db"
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
ENV PATH="$PATH:/home/filebrowser"
|
||||
RUN apk --no-cache add ca-certificates mailcap tzdata
|
||||
RUN adduser -D -s /bin/true -u 1000 filebrowser
|
||||
|
||||
@@ -15,4 +15,4 @@ COPY [ "./backend/http/embed/", "./http/dist/"]
|
||||
|
||||
# Run tests
|
||||
RUN ./filebrowser set -u testuser1,testuser1
|
||||
RUN FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -13,4 +13,4 @@ RUN mkdir -p ./http/dist
|
||||
COPY [ "./backend/http/embed/", "./http/dist/"]
|
||||
|
||||
# Run tests
|
||||
RUN FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -10,4 +10,4 @@ COPY ["./backend/filebrowser", "./"]
|
||||
COPY [ "./backend/http/embed/", "./http/dist/"]
|
||||
# Run tests
|
||||
WORKDIR /app/frontend
|
||||
RUN node mock-oidc-server.js & sleep 2 && cd ../backend/ && FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
RUN node mock-oidc-server.js & sleep 2 && cd ../backend/ && ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -9,5 +9,6 @@ RUN mkdir -p ./http/dist
|
||||
COPY [ "./backend/http/embed/", "./http/dist/"]
|
||||
RUN apt update && apt install nginx -y
|
||||
RUN mv default.conf /etc/nginx/conf.d/default.conf
|
||||
RUN mv .htpasswd /etc/nginx/conf.d/.htpasswd
|
||||
RUN sed -i 's/filebrowser/localhost/g' /etc/nginx/conf.d/default.conf
|
||||
RUN nginx & FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
RUN nginx & ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -14,4 +14,4 @@ RUN mkdir -p ./http/dist
|
||||
COPY [ "./backend/http/embed/", "./http/dist/"]
|
||||
|
||||
# Run tests
|
||||
RUN FILEBROWSER_NO_EMBEDED=true ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
|
||||
@@ -4,6 +4,7 @@ ARG REVISION
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
RUN apk update && apk add upx
|
||||
ENV GOEXPERIMENT=greenteagc
|
||||
RUN go build -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
|
||||
@@ -18,7 +19,6 @@ COPY ./frontend/ ./
|
||||
RUN npm run build-docker
|
||||
|
||||
FROM alpine:latest
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
ENV FILEBROWSER_DATABASE="/home/filebrowser/data/database.db"
|
||||
ENV PATH="$PATH:/home/filebrowser"
|
||||
RUN apk --no-cache add ca-certificates mailcap tzdata
|
||||
|
||||
@@ -6,6 +6,7 @@ services:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./src/proxy/backend/default.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./src/proxy/backend/.htpasswd:/etc/nginx/conf.d/.htpasswd
|
||||
filebrowser:
|
||||
hostname: filebrowser
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
demo-127.0.0.1:$apr1$j5FRA4Bc$RYdNFuM7YAAo9Qtie8w4b/
|
||||
@@ -1,6 +1,6 @@
|
||||
server:
|
||||
port: 8080
|
||||
baseURL: "/"
|
||||
baseURL: "/subpath"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
@@ -32,5 +32,5 @@ userDefaults:
|
||||
permissions:
|
||||
admin: false
|
||||
modify: true
|
||||
share: false
|
||||
share: true
|
||||
api: false
|
||||
@@ -7,13 +7,25 @@ server {
|
||||
listen 80;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
location / {
|
||||
location /subpath/public/ {
|
||||
proxy_pass http://filebrowser:8080/subpath/public/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# requires basic auth demo-127.0.0.1:SecurePass123!
|
||||
location /subpath {
|
||||
auth_basic "Restricted Access";
|
||||
auth_basic_user_file /etc/nginx/conf.d/.htpasswd;
|
||||
proxy_set_header X-Username $uuid;
|
||||
add_header X-Username $uuid;
|
||||
proxy_pass http://filebrowser:8080;
|
||||
proxy_pass http://filebrowser:8080/subpath;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
//globalSetup: "./global-setup",
|
||||
globalSetup: "./tests/playwright/proxy-setup.ts",
|
||||
timeout: 3000,
|
||||
testDir: "./tests/playwright/proxy",
|
||||
/* Run tests in files in parallel */
|
||||
@@ -25,8 +25,8 @@ export default defineConfig({
|
||||
reporter: "line",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
actionTimeout: 3000,
|
||||
//storageState: "loginAuth.json",
|
||||
actionTimeout: 5000,
|
||||
storageState: "loginAuth.json",
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1",
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ server:
|
||||
- path: "."
|
||||
name: "docker"
|
||||
config:
|
||||
private: true
|
||||
defaultEnabled: true
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
cmd = "go build -o ./tmp/filebrowser --tags=mupdf --ldflags=\"-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'\" ."
|
||||
bin = "tmp/filebrowser"
|
||||
full_bin = "FILEBROWSER_DEVMODE=true CGO_ENABLED=1 ./tmp/filebrowser -c test_config.yaml"
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
exclude_dir = ["tmp", "vendor", "http/dist", "http/embed", "swagger/docs"]
|
||||
log = "air.log"
|
||||
delay = 1000 # ms
|
||||
|
||||
[log]
|
||||
time = true
|
||||
|
||||
[misc]
|
||||
clean_on_exit = true
|
||||
@@ -0,0 +1,5 @@
|
||||
tmp/
|
||||
air.log
|
||||
filebrowser
|
||||
generated.yaml
|
||||
test_config.yaml
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
@@ -73,7 +74,7 @@ func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error
|
||||
}
|
||||
response.FileInfo = *info
|
||||
response.RealPath = realPath
|
||||
response.Source = opts.Source
|
||||
response.Source = index.Name
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && iteminfo.IsOnlyOffice(info.Name) {
|
||||
response.OnlyOfficeId = generateOfficeId(realPath)
|
||||
}
|
||||
@@ -188,7 +189,7 @@ func validateMoveDestination(src, dst string, isSrcDir bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, realdst string) error {
|
||||
func MoveResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, realdst string, s *share.Storage) error {
|
||||
// Validate the move operation before executing
|
||||
if err := validateMoveDestination(realsrc, realdst, isSrcDir); err != nil {
|
||||
return err
|
||||
@@ -198,8 +199,21 @@ func MoveResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, rea
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go RefreshIndex(sourceIndex, realsrc, isSrcDir) //nolint:errcheck
|
||||
go RefreshIndex(sourceIndex, realdst, isDestDir) //nolint:errcheck
|
||||
go RefreshIndex(sourceIndex, realsrc, isSrcDir) //nolint:errcheck
|
||||
go RefreshIndex(destIndex, realdst, isDestDir) //nolint:errcheck
|
||||
|
||||
// update shares
|
||||
idx := indexing.GetIndex(sourceIndex)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
idx2 := indexing.GetIndex(destIndex)
|
||||
if idx2 == nil {
|
||||
return fmt.Errorf("could not get index: %v ", destIndex)
|
||||
}
|
||||
|
||||
// Use backend source paths to match how shares are stored
|
||||
go s.UpdateShares(idx.Path, idx.MakeIndexPath(realsrc), idx2.Path, idx2.MakeIndexPath(realdst)) //nolint:errcheck
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -213,8 +227,9 @@ func CopyResource(isSrcDir, isDestDir bool, sourceIndex, destIndex, realsrc, rea
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go RefreshIndex(sourceIndex, realsrc, isSrcDir) //nolint:errcheck
|
||||
go RefreshIndex(sourceIndex, realdst, isDestDir) //nolint:errcheck
|
||||
go RefreshIndex(sourceIndex, realsrc, isSrcDir) //nolint:errcheck
|
||||
go RefreshIndex(destIndex, realdst, isDestDir) //nolint:errcheck
|
||||
// update shares
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -2,10 +2,13 @@ package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"log"
|
||||
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
// StorageBackend is a storage backend for auth storage.
|
||||
@@ -35,9 +38,19 @@ func NewStorage(back StorageBackend, userStore *users.Storage) (*Storage, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptionKey, err = base64.StdEncoding.DecodeString(settings.Config.Auth.TotpSecret)
|
||||
if err != nil {
|
||||
logger.Warning("failed to decode TOTP secret, using default key. This is insecure and should be fixed.")
|
||||
if settings.Config.Auth.TotpSecret != "" {
|
||||
key, err := base64.StdEncoding.DecodeString(settings.Config.Auth.TotpSecret)
|
||||
keyLen := len(key)
|
||||
|
||||
if err == nil && (keyLen == 16 || keyLen == 24 || keyLen == 25) {
|
||||
// Use the user-provided key if it's valid
|
||||
encryptionKey = key
|
||||
} else {
|
||||
// Otherwise, derive the key from the provided secret string
|
||||
log.Println("[WARN] totpSecret is not a valid Base64 encoded key. Deriving a key from it. For better security, generate a secret with 'openssl rand -base64 32'.")
|
||||
salt := []byte{0xda, 0x90, 0x45, 0xc3, 0x06, 0xb5, 0x99, 0x9f, 0xb6, 0xae, 0xfc, 0x14, 0xef, 0x27, 0x6e, 0x6a}
|
||||
encryptionKey = pbkdf2.Key([]byte(settings.Config.Auth.TotpSecret), salt, 4096, 32, sha256.New)
|
||||
}
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ func Initialize(configFile string) {
|
||||
setupAuth(false)
|
||||
setupSources(false)
|
||||
setupUrls()
|
||||
setupFrontend()
|
||||
setupFrontend(false)
|
||||
}
|
||||
|
||||
func setupFrontend() {
|
||||
func setupFrontend(generate bool) {
|
||||
if !Config.Frontend.DisableDefaultLinks {
|
||||
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
|
||||
Text: fmt.Sprintf("(%v)", version.Version),
|
||||
@@ -73,6 +73,12 @@ func setupFrontend() {
|
||||
// check if file exists
|
||||
if _, err := os.Stat("reduce-rounded-corners.css"); err == nil {
|
||||
addCustomTheme("alternative", "Reduce rounded corners", "reduce-rounded-corners.css")
|
||||
if generate {
|
||||
Config.Frontend.Styling.CustomThemes["alternative"] = CustomTheme{
|
||||
Description: "Reduce rounded corners",
|
||||
CSS: "reduce-rounded-corners.css",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_, ok := Config.Frontend.Styling.CustomThemes["default"]
|
||||
@@ -98,6 +104,9 @@ func setupSources(generate bool) {
|
||||
logger.Fatal("There are no `server.sources` configured. If you have `server.root` configured, please update the config and add at least one `server.sources` with a `path` configured.")
|
||||
} else {
|
||||
for k, source := range Config.Server.Sources {
|
||||
if source.Config.Disabled {
|
||||
continue
|
||||
}
|
||||
realPath := getRealPath(source.Path)
|
||||
name := filepath.Base(realPath)
|
||||
if name == "\\" {
|
||||
@@ -216,8 +225,14 @@ func setupLogging() {
|
||||
}
|
||||
}
|
||||
for _, logConfig := range Config.Server.Logging {
|
||||
// Enable debug logging automatically in dev mode
|
||||
levels := logConfig.Levels
|
||||
if os.Getenv("FILEBROWSER_DEVMODE") == "true" {
|
||||
levels = "info|warning|error|debug"
|
||||
}
|
||||
|
||||
logConfig := logger.JsonConfig{
|
||||
Levels: logConfig.Levels,
|
||||
Levels: levels,
|
||||
ApiLevels: logConfig.ApiLevels,
|
||||
Output: logConfig.Output,
|
||||
Utc: logConfig.Utc,
|
||||
|
||||
@@ -185,7 +185,7 @@ func GenerateYaml() {
|
||||
setupAuth(true)
|
||||
setupSources(true)
|
||||
setupUrls()
|
||||
setupFrontend()
|
||||
setupFrontend(true)
|
||||
input := "common/settings/settings.go" // "path to Go source file or directory containing structs"
|
||||
output := "generated.yaml" // "output YAML file"
|
||||
|
||||
|
||||
@@ -77,7 +77,10 @@ type Source struct {
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in seconds to re-index the source
|
||||
DenyByDefault bool `json:"denyByDefault"` // deny access unless an "allow" access rule was specifically created.
|
||||
Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted.
|
||||
Disabled bool `json:"disabled"` // disable the source, this is useful so you don't need to remove it from the config file
|
||||
IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in minutes to re-index the source
|
||||
DisableIndexing bool `json:"disableIndexing"` // disable the indexing of this source
|
||||
MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported
|
||||
NeverWatchPaths []string `json:"neverWatchPaths"` // paths that get initially once. Useful for folders that rarely change contents (without source path prefix)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
@@ -52,9 +53,10 @@ type FrontendRuleSet struct {
|
||||
}
|
||||
|
||||
type FrontendAccessRule struct {
|
||||
DenyAll bool `json:"denyAll,omitempty"`
|
||||
Deny FrontendRuleSet `json:"deny"`
|
||||
Allow FrontendRuleSet `json:"allow"`
|
||||
DenyAll bool `json:"denyAll,omitempty"`
|
||||
Deny FrontendRuleSet `json:"deny"`
|
||||
Allow FrontendRuleSet `json:"allow"`
|
||||
SourceDenyDefault bool `json:"sourceDenyDefault"`
|
||||
}
|
||||
|
||||
// GroupMap maps group names to a set of usernames.
|
||||
@@ -63,8 +65,8 @@ type GroupMap map[string]StringSet
|
||||
// Storage manages access rules and group membership.
|
||||
type Storage struct {
|
||||
mux sync.RWMutex
|
||||
AllRules SourceRuleMap // AllRules[sourcePath][indexPath]
|
||||
Groups GroupMap // key: group name, value: set of usernames
|
||||
AllRules SourceRuleMap // AllRules[sourcePath][indexPath] - in-memory authoritative state
|
||||
Groups GroupMap // key: group name, value: set of usernames - in-memory authoritative state
|
||||
DB *storm.DB // Optional: DB for persistence
|
||||
Users *users.Storage // Reference to users storage
|
||||
}
|
||||
@@ -84,6 +86,12 @@ func (s *Storage) SaveToDB() error {
|
||||
return s.DB.Set(accessRulesBucket, accessRulesKey, data)
|
||||
}
|
||||
|
||||
// Flush persists the current in-memory state to the backing store.
|
||||
// Call during graceful shutdown to ensure DB matches memory.
|
||||
func (s *Storage) Flush() error {
|
||||
return s.SaveToDB()
|
||||
}
|
||||
|
||||
// LoadFromDB loads all rules from the DB if DB is set.
|
||||
func (s *Storage) LoadFromDB() error {
|
||||
if s.DB == nil {
|
||||
@@ -128,6 +136,12 @@ func NewStorage(db *storm.DB, usersStore *users.Storage) *Storage {
|
||||
return s
|
||||
}
|
||||
|
||||
// ClearCache clears the access cache (useful for testing)
|
||||
func ClearCache() {
|
||||
// Recreate the cache to clear it
|
||||
accessCache = cache.NewCache(1 * time.Minute)
|
||||
}
|
||||
|
||||
// getOrCreateRuleNL ensures a rule exists for the given source and index path.
|
||||
// The caller must hold the lock.
|
||||
func (s *Storage) getOrCreateRuleNL(sourcePath, indexPath string) *AccessRule {
|
||||
@@ -233,6 +247,7 @@ func (s *Storage) DenyAll(sourcePath, indexPath string) error {
|
||||
|
||||
// Permitted checks if a username is permitted for a given sourcePath and indexPath, recursively checking parent directories.
|
||||
func (s *Storage) Permitted(sourcePath, indexPath, username string) bool {
|
||||
|
||||
// Get current version for the sourcePath
|
||||
versionKey := "version:" + sourcePath
|
||||
version := 0
|
||||
@@ -263,6 +278,19 @@ func (s *Storage) computePermitted(sourcePath, indexPath, username string) bool
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// No rules found anywhere in the path hierarchy
|
||||
// Check if the source has DenyByDefault configured (acts like a root-level denyAll rule)
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
|
||||
if !ok {
|
||||
logger.Errorf("source %s not found in config during access check", sourcePath)
|
||||
return false
|
||||
}
|
||||
|
||||
if sourceInfo.Config.DenyByDefault {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -279,28 +307,27 @@ func (s *Storage) permittedAtExactPath(sourcePath, indexPath, username string) (
|
||||
if !ok {
|
||||
return true, false
|
||||
}
|
||||
return s.permittedRule(rule, username), true
|
||||
return s.permittedRule(rule, username, sourcePath), true
|
||||
}
|
||||
|
||||
// permittedRule contains the old Permitted logic, but operates on a rule and username only.
|
||||
func (s *Storage) permittedRule(rule *AccessRule, username string) bool {
|
||||
// Check user deny
|
||||
func (s *Storage) permittedRule(rule *AccessRule, username string, sourcePath string) bool {
|
||||
// Check user deny first - this always takes precedence
|
||||
if _, found := rule.Deny.Users[username]; found {
|
||||
return false
|
||||
}
|
||||
if rule.DenyAll {
|
||||
return false
|
||||
}
|
||||
// Check group deny
|
||||
for group := range rule.Deny.Groups {
|
||||
if s.isUserInGroup(username, group) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// If any allow is present, user must be in at least one
|
||||
|
||||
// Check allow rules - these can override DenyAll
|
||||
hasUserAllow := len(rule.Allow.Users) > 0
|
||||
hasGroupAllow := len(rule.Allow.Groups) > 0
|
||||
if hasUserAllow || hasGroupAllow {
|
||||
// If user is in any allow list, permit them (overrides DenyAll)
|
||||
if hasUserAllow {
|
||||
if _, found := rule.Allow.Users[username]; found {
|
||||
return true
|
||||
@@ -313,18 +340,30 @@ func (s *Storage) permittedRule(rule *AccessRule, username string) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
// Fallback to deny rules
|
||||
if _, found := rule.Deny.Users[username]; found {
|
||||
return false
|
||||
}
|
||||
for group := range rule.Deny.Groups {
|
||||
if s.isUserInGroup(username, group) {
|
||||
|
||||
// User is not in any allow list
|
||||
// Check DenyByDefault to determine behavior
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
|
||||
if ok && sourceInfo.Config.DenyByDefault {
|
||||
// DenyByDefault=true: allow lists are exclusive, deny users not on the list
|
||||
return false
|
||||
} else {
|
||||
// DenyByDefault=false: allow lists are additive, allow users not on the list
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check DenyAll - only applies if no allow rules exist
|
||||
if rule.DenyAll {
|
||||
return false
|
||||
}
|
||||
|
||||
// No specific rules apply - check source DenyByDefault setting
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
|
||||
if ok && sourceInfo.Config.DenyByDefault {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -344,7 +383,16 @@ func (s *Storage) isUserInGroup(username, group string) bool {
|
||||
func (s *Storage) GetFrontendRules(sourcePath, indexPath string) (FrontendAccessRule, bool) {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
frontendRules := FrontendAccessRule{
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: make([]string, 0),
|
||||
Groups: make([]string, 0),
|
||||
@@ -382,6 +430,14 @@ func (s *Storage) GetAllRules(sourcePath string) (map[string]FrontendAccessRule,
|
||||
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
// Return a copy to avoid external mutation
|
||||
frontendRules := make(map[string]FrontendAccessRule, len(s.AllRules))
|
||||
rules, ok := s.AllRules[sourcePath]
|
||||
@@ -391,7 +447,8 @@ func (s *Storage) GetAllRules(sourcePath string) (map[string]FrontendAccessRule,
|
||||
for indexPath, rule := range rules {
|
||||
// Convert AccessRule to FrontendAccessRule
|
||||
frontendRules[indexPath] = FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
@@ -733,6 +790,14 @@ func (s *Storage) RemoveAllRulesForGroup(groupname string) error {
|
||||
func (s *Storage) GetRulesForUser(sourcePath, username string) map[string]FrontendAccessRule {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
userRules := make(map[string]FrontendAccessRule)
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
@@ -750,7 +815,8 @@ func (s *Storage) GetRulesForUser(sourcePath, username string) map[string]Fronte
|
||||
}
|
||||
if userHasRule {
|
||||
userRules[indexPath] = FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
@@ -769,6 +835,14 @@ func (s *Storage) GetRulesForUser(sourcePath, username string) map[string]Fronte
|
||||
func (s *Storage) GetRulesForGroup(sourcePath, groupname string) map[string]FrontendAccessRule {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
groupRules := make(map[string]FrontendAccessRule)
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
@@ -786,7 +860,8 @@ func (s *Storage) GetRulesForGroup(sourcePath, groupname string) map[string]Fron
|
||||
}
|
||||
if groupHasRule {
|
||||
groupRules[indexPath] = FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
@@ -805,6 +880,14 @@ func (s *Storage) GetRulesForGroup(sourcePath, groupname string) map[string]Fron
|
||||
func (s *Storage) GetAllRulesByUsers(sourcePath string) map[string]map[string]FrontendAccessRule {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
allUserRules := make(map[string]map[string]FrontendAccessRule)
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
@@ -817,7 +900,8 @@ func (s *Storage) GetAllRulesByUsers(sourcePath string) map[string]map[string]Fr
|
||||
continue
|
||||
}
|
||||
frontendRule := FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
@@ -847,6 +931,14 @@ func (s *Storage) GetAllRulesByUsers(sourcePath string) map[string]map[string]Fr
|
||||
func (s *Storage) GetAllRulesByGroups(sourcePath string) map[string]map[string]FrontendAccessRule {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
|
||||
// Get source configuration
|
||||
sourceDenyDefault := false
|
||||
sourceInfo, sourceExists := settings.Config.Server.SourceMap[sourcePath]
|
||||
if sourceExists {
|
||||
sourceDenyDefault = sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
allGroupRules := make(map[string]map[string]FrontendAccessRule)
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
@@ -859,7 +951,8 @@ func (s *Storage) GetAllRulesByGroups(sourcePath string) map[string]map[string]F
|
||||
continue
|
||||
}
|
||||
frontendRule := FrontendAccessRule{
|
||||
DenyAll: rule.DenyAll,
|
||||
DenyAll: rule.DenyAll,
|
||||
SourceDenyDefault: sourceDenyDefault,
|
||||
Deny: FrontendRuleSet{
|
||||
Users: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Users))),
|
||||
Groups: utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups))),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/asdine/storm/v3"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/access"
|
||||
boltusers "github.com/gtsteffaniak/filebrowser/backend/database/storage/bolt"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
@@ -29,7 +30,28 @@ func createTestUser(t *testing.T, userStore *users.Storage, username string) {
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestSources() {
|
||||
// Setup default test sources with allow-by-default behavior
|
||||
settings.Config.Server.SourceMap = map[string]settings.Source{
|
||||
"mnt/storage": {
|
||||
Path: "mnt/storage",
|
||||
Name: "storage",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: false,
|
||||
},
|
||||
},
|
||||
"mnt/open": {
|
||||
Path: "mnt/open",
|
||||
Name: "open",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_UserBlacklist(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -49,6 +71,7 @@ func TestPermitted_UserBlacklist(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermitted_UserWhitelist(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -63,11 +86,12 @@ func TestPermitted_UserWhitelist(t *testing.T) {
|
||||
t.Error("bob should be permitted (allowed)")
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/vip", "alice") {
|
||||
t.Error("alice should be permitted (not denied)")
|
||||
t.Error("alice should be permitted (default allow behavior when DenyByDefault=false)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_GroupBlacklist(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -88,6 +112,7 @@ func TestPermitted_GroupBlacklist(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermitted_GroupWhitelist(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -103,11 +128,12 @@ func TestPermitted_GroupWhitelist(t *testing.T) {
|
||||
t.Error("bob should be permitted (in allowed group)")
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/vip", "alice") {
|
||||
t.Error("alice should be permitted (not in denied group)")
|
||||
t.Error("alice should be permitted (default allow behavior when DenyByDefault=false)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_NoRule(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, _ := createTestStorage(t)
|
||||
err := s.LoadFromDB()
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
@@ -119,6 +145,7 @@ func TestPermitted_NoRule(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermitted_CombinedRules(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -163,6 +190,7 @@ func TestPermitted_CombinedRules(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermitted_DenyAll(t *testing.T) {
|
||||
setupTestSources()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
@@ -182,12 +210,12 @@ func TestPermitted_DenyAll(t *testing.T) {
|
||||
t.Error("bob should not be permitted (deny all)")
|
||||
}
|
||||
|
||||
// Test that Allow rule is overridden by DenyAll
|
||||
// Test that Allow rule overrides DenyAll
|
||||
if err = s.AllowUser("mnt/storage", "/private", "alice"); err != nil {
|
||||
t.Errorf("AllowUser failed: %v", err)
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/private", "alice") {
|
||||
t.Error("alice should not be permitted (deny all overrides allow)")
|
||||
if !s.Permitted("mnt/storage", "/private", "alice") {
|
||||
t.Error("alice should be permitted (allow overrides deny all)")
|
||||
}
|
||||
|
||||
// Test removing DenyAll
|
||||
@@ -204,8 +232,176 @@ func TestPermitted_DenyAll(t *testing.T) {
|
||||
t.Error("alice should be permitted after removing deny all")
|
||||
}
|
||||
|
||||
// Bob should be permitted as he is not on any list, and there is an allow list which he is not on, but he is not on deny either.
|
||||
// Bob should be permitted because DenyByDefault=false makes allow lists additive (not exclusive)
|
||||
if !s.Permitted("mnt/storage", "/private", "bob") {
|
||||
t.Error("bob should be permitted after removing deny all (not on allow list but not denied)")
|
||||
t.Error("bob should be permitted after removing deny all (DenyByDefault=false makes allow lists additive)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_DenyByDefault(t *testing.T) {
|
||||
// Clear access cache to prevent test pollution
|
||||
access.ClearCache()
|
||||
|
||||
// Create isolated storage for this test
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
err := s.LoadFromDB()
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
t.Errorf("unexpected error loading from DB: %v", err)
|
||||
}
|
||||
|
||||
// Setup test configuration with DenyByDefault enabled for one source
|
||||
originalSourceMap := settings.Config.Server.SourceMap
|
||||
defer func() {
|
||||
settings.Config.Server.SourceMap = originalSourceMap
|
||||
access.ClearCache()
|
||||
}()
|
||||
|
||||
settings.Config.Server.SourceMap = map[string]settings.Source{
|
||||
"mnt/storage": {
|
||||
Path: "mnt/storage",
|
||||
Name: "storage",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: true,
|
||||
},
|
||||
},
|
||||
"mnt/open": {
|
||||
Path: "mnt/open",
|
||||
Name: "open",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test DenyByDefault: When no rules exist, users should be denied
|
||||
if s.Permitted("mnt/storage", "/private", "alice") {
|
||||
t.Error("alice should not be permitted (DenyByDefault is true and no rules exist)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/private", "bob") {
|
||||
t.Error("bob should not be permitted (DenyByDefault is true and no rules exist)")
|
||||
}
|
||||
|
||||
// Test that explicit allow rules override DenyByDefault
|
||||
if err := s.AllowUser("mnt/storage", "/allowed", "alice"); err != nil {
|
||||
t.Errorf("AllowUser failed: %v", err)
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/allowed", "alice") {
|
||||
t.Error("alice should be permitted (explicit allow rule overrides DenyByDefault)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/allowed", "bob") {
|
||||
t.Error("bob should not be permitted (not in allow list and DenyByDefault is true)")
|
||||
}
|
||||
|
||||
// Test that explicit deny rules work with DenyByDefault
|
||||
if err := s.DenyUser("mnt/storage", "/denied", "alice"); err != nil {
|
||||
t.Errorf("DenyUser failed: %v", err)
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/denied", "alice") {
|
||||
t.Error("alice should not be permitted (explicit deny rule)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/denied", "bob") {
|
||||
t.Error("bob should not be permitted (DenyByDefault is true)")
|
||||
}
|
||||
|
||||
// Test that DenyByDefault doesn't affect sources where it's disabled
|
||||
if !s.Permitted("mnt/open", "/public", "alice") {
|
||||
t.Error("alice should be permitted in open source (DenyByDefault is false)")
|
||||
}
|
||||
if !s.Permitted("mnt/open", "/public", "bob") {
|
||||
t.Error("bob should be permitted in open source (DenyByDefault is false)")
|
||||
}
|
||||
|
||||
// Test recursive path checking with DenyByDefault
|
||||
// Allow alice at parent path, should work for child paths too
|
||||
if err := s.AllowUser("mnt/storage", "/parent", "alice"); err != nil {
|
||||
t.Errorf("AllowUser failed: %v", err)
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/parent/child", "alice") {
|
||||
t.Error("alice should be permitted for child path (inherits from parent allow rule)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/parent/child", "bob") {
|
||||
t.Error("bob should not be permitted for child path (DenyByDefault and not in allow list)")
|
||||
}
|
||||
|
||||
// Test that group rules work with DenyByDefault
|
||||
_ = s.AddUserToGroup("vip", "alice")
|
||||
if err := s.AllowGroup("mnt/storage", "/vip", "vip"); err != nil {
|
||||
t.Errorf("AllowGroup failed: %v", err)
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/vip", "alice") {
|
||||
t.Error("alice should be permitted (in allowed group)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/vip", "bob") {
|
||||
t.Error("bob should not be permitted (not in allowed group and DenyByDefault is true)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_DenyByDefaultWithDenyAll(t *testing.T) {
|
||||
// Clear access cache to prevent test pollution
|
||||
access.ClearCache()
|
||||
|
||||
// Create isolated storage for this test
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "alice")
|
||||
createTestUser(t, userStore, "bob")
|
||||
err := s.LoadFromDB()
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
t.Errorf("unexpected error loading from DB: %v", err)
|
||||
}
|
||||
|
||||
// Setup test configuration with DenyByDefault enabled
|
||||
originalSourceMap := settings.Config.Server.SourceMap
|
||||
defer func() {
|
||||
settings.Config.Server.SourceMap = originalSourceMap
|
||||
access.ClearCache()
|
||||
}()
|
||||
|
||||
settings.Config.Server.SourceMap = map[string]settings.Source{
|
||||
"mnt/storage": {
|
||||
Path: "mnt/storage",
|
||||
Name: "storage",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test that explicit DenyAll rule also works when DenyByDefault is enabled
|
||||
if err = s.DenyAll("mnt/storage", "/restricted"); err != nil {
|
||||
t.Errorf("DenyAll failed: %v", err)
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/restricted", "alice") {
|
||||
t.Error("alice should not be permitted (DenyAll rule)")
|
||||
}
|
||||
if s.Permitted("mnt/storage", "/restricted", "bob") {
|
||||
t.Error("bob should not be permitted (DenyAll rule)")
|
||||
}
|
||||
|
||||
// Test that Allow rule overrides DenyAll even with DenyByDefault
|
||||
if err = s.AllowUser("mnt/storage", "/restricted", "alice"); err != nil {
|
||||
t.Errorf("AllowUser failed: %v", err)
|
||||
}
|
||||
if !s.Permitted("mnt/storage", "/restricted", "alice") {
|
||||
t.Error("alice should be permitted (allow overrides DenyAll)")
|
||||
}
|
||||
|
||||
// Remove DenyAll and test that DenyByDefault takes effect
|
||||
removed, err := s.RemoveDenyAll("mnt/storage", "/restricted")
|
||||
if err != nil {
|
||||
t.Errorf("RemoveDenyAll failed: %v", err)
|
||||
}
|
||||
if !removed {
|
||||
t.Error("RemoveDenyAll should have removed the rule")
|
||||
}
|
||||
|
||||
// After removing DenyAll, alice should be permitted due to Allow rule
|
||||
if !s.Permitted("mnt/storage", "/restricted", "alice") {
|
||||
t.Error("alice should be permitted after removing DenyAll (has allow rule)")
|
||||
}
|
||||
// Bob should not be permitted due to DenyByDefault
|
||||
if s.Permitted("mnt/storage", "/restricted", "bob") {
|
||||
t.Error("bob should not be permitted (DenyByDefault is true and no allow rule)")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import "sync"
|
||||
type CommonShare struct {
|
||||
//AllowEdit bool `json:"allowEdit,omitempty"`
|
||||
AllowUpload bool `json:"allowUpload,omitempty"`
|
||||
DisablingFileViewer bool `json:"disableFileViewer,omitempty"`
|
||||
DisableFileViewer bool `json:"disableFileViewer,omitempty"`
|
||||
DownloadsLimit int `json:"downloadsLimit,omitempty"`
|
||||
ShareTheme string `json:"shareTheme,omitempty"`
|
||||
DisableAnonymous bool `json:"disableAnonymous,omitempty"`
|
||||
@@ -20,25 +20,27 @@ type CommonShare struct {
|
||||
Favicon string `json:"favicon,omitempty"`
|
||||
QuickDownload bool `json:"quickDownload,omitempty"`
|
||||
HideNavButtons bool `json:"hideNavButtons,omitempty"`
|
||||
DisableSidebar bool `json:"disableSidebar"`
|
||||
ViewMode string `json:"viewMode,omitempty"`
|
||||
Source string `json:"source,omitempty"` // backend source is path to maintain between name changes
|
||||
Path string `json:"path,omitempty"`
|
||||
DownloadURL string `json:"downloadURL,omitempty"`
|
||||
DisableShareCard bool `json:"disableShareCard,omitempty"`
|
||||
}
|
||||
type CreateBody struct {
|
||||
CommonShare
|
||||
Hash string `json:"hash,omitempty"`
|
||||
SourceName string `json:"sourceName,omitempty"`
|
||||
Password string `json:"password"`
|
||||
Expires string `json:"expires"`
|
||||
Unit string `json:"unit"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Password string `json:"password"`
|
||||
Expires string `json:"expires"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
|
||||
// Link is the information needed to build a shareable link.
|
||||
type Link struct {
|
||||
CommonShare
|
||||
Mu sync.Mutex `json:"-"`
|
||||
Downloads int `json:"-"`
|
||||
Downloads int `json:"downloads"`
|
||||
Hash string `json:"hash" storm:"id,index"`
|
||||
Path string `json:"path" storm:"index"`
|
||||
Source string `json:"source" storm:"index"`
|
||||
UserID uint `json:"userID"`
|
||||
Expire int64 `json:"expire"`
|
||||
PasswordHash string `json:"password_hash,omitempty"`
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/crud"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
|
||||
// StorageBackend is the interface to implement for a share storage.
|
||||
@@ -13,6 +17,7 @@ type StorageBackend interface {
|
||||
FindByUserID(id uint) ([]*Link, error)
|
||||
GetByHash(hash string) (*Link, error)
|
||||
GetPermanent(path, source string, id uint) (*Link, error)
|
||||
GetBySourcePath(path, source string) ([]*Link, error)
|
||||
Gets(path, source string, id uint) ([]*Link, error)
|
||||
Save(s *Link) error
|
||||
Delete(hash string) error
|
||||
@@ -49,15 +54,20 @@ func (c *crudBackend) DeleteByID(id any) error {
|
||||
|
||||
// Storage is a share storage using generics.
|
||||
type Storage struct {
|
||||
Generic *crud.Storage[Link]
|
||||
back StorageBackend
|
||||
Generic *crud.Storage[Link]
|
||||
back StorageBackend
|
||||
shareCache map[string]*Link
|
||||
mu sync.RWMutex
|
||||
users *users.Storage
|
||||
}
|
||||
|
||||
// NewStorage creates a share links storage from a backend.
|
||||
func NewStorage(back StorageBackend) *Storage {
|
||||
func NewStorage(back StorageBackend, usersStore *users.Storage) *Storage {
|
||||
return &Storage{
|
||||
Generic: crud.NewStorage[Link](&crudBackend{back: back}),
|
||||
back: back,
|
||||
Generic: crud.NewStorage[Link](&crudBackend{back: back}),
|
||||
back: back,
|
||||
shareCache: make(map[string]*Link),
|
||||
users: usersStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +77,24 @@ func (s *Storage) All() ([]*Link, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.filterExpired(links)
|
||||
filtered, err := s.filterExpired(links)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
for i, l := range filtered {
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
|
||||
filtered[i] = existing
|
||||
} else {
|
||||
s.shareCache[l.Hash] = l
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// FindByUserID wraps StorageBackend.FindByUserID and handles expiry.
|
||||
@@ -76,11 +103,43 @@ func (s *Storage) FindByUserID(id uint) ([]*Link, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.filterExpired(links)
|
||||
filtered, err := s.filterExpired(links)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
for i, l := range filtered {
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
|
||||
filtered[i] = existing
|
||||
} else {
|
||||
s.shareCache[l.Hash] = l
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// GetByHash wraps StorageBackend.GetByHash and handles expiry.
|
||||
func (s *Storage) GetByHash(hash string) (*Link, error) {
|
||||
// return stable in-memory pointer if available
|
||||
s.mu.RLock()
|
||||
if link, ok := s.shareCache[hash]; ok && link != nil {
|
||||
s.mu.RUnlock()
|
||||
if link.Expire != 0 && link.Expire <= time.Now().Unix() {
|
||||
_ = s.back.Delete(hash)
|
||||
s.mu.Lock()
|
||||
delete(s.shareCache, hash)
|
||||
s.mu.Unlock()
|
||||
return nil, errors.ErrNotExist
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
link, err := s.back.GetByHash(hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -89,12 +148,23 @@ func (s *Storage) GetByHash(hash string) (*Link, error) {
|
||||
_ = s.back.Delete(hash)
|
||||
return nil, errors.ErrNotExist
|
||||
}
|
||||
s.mu.Lock()
|
||||
|
||||
s.shareCache[hash] = link
|
||||
s.mu.Unlock()
|
||||
return link, nil
|
||||
}
|
||||
|
||||
// GetPermanent wraps StorageBackend.GetPermanent
|
||||
func (s *Storage) GetPermanent(path, source string, id uint) (*Link, error) {
|
||||
return s.back.GetPermanent(path, source, id)
|
||||
l, err := s.back.GetPermanent(path, source, id)
|
||||
if err == nil && l != nil {
|
||||
s.mu.Lock()
|
||||
|
||||
s.shareCache[l.Hash] = l
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return l, err
|
||||
}
|
||||
|
||||
// Gets wraps StorageBackend.Gets and handles expiry.
|
||||
@@ -103,17 +173,134 @@ func (s *Storage) Gets(sourcePath, source string, id uint) ([]*Link, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.filterExpired(links)
|
||||
filtered, err := s.filterExpired(links)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
for i, l := range filtered {
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
|
||||
filtered[i] = existing
|
||||
} else {
|
||||
s.shareCache[l.Hash] = l
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// GetBySourcePath wraps StorageBackend.GetBySourcePath and handles expiry.
|
||||
func (s *Storage) GetBySourcePath(path, source string) ([]*Link, error) {
|
||||
links, err := s.back.GetBySourcePath(path, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filtered, err := s.filterExpired(links)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
for i, l := range filtered {
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
|
||||
filtered[i] = existing
|
||||
} else {
|
||||
s.shareCache[l.Hash] = l
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// UpdateShares updates all shares that exactly match oldSource and oldPath
|
||||
// to point to newSource and newPath. Returns the number of updated shares.
|
||||
func (s *Storage) UpdateShares(oldSource, oldPath, newSource, newPath string) (int, error) {
|
||||
links, err := s.back.All()
|
||||
if err != nil && err != errors.ErrNotExist {
|
||||
logger.Error("failed to list shares", "error", err)
|
||||
return 0, err
|
||||
}
|
||||
updated := 0
|
||||
for _, l := range links {
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
if l.Source != oldSource {
|
||||
continue
|
||||
}
|
||||
pos := strings.Index(l.Path, oldPath)
|
||||
if pos < 0 {
|
||||
continue
|
||||
}
|
||||
oldFullPath := l.Path
|
||||
l.Source = newSource
|
||||
l.Path = oldFullPath[:pos] + newPath + oldFullPath[pos+len(oldPath):]
|
||||
if err := s.back.Save(l); err != nil {
|
||||
logger.Error("failed to save updated share", "hash", l.Hash, "error", err)
|
||||
return updated, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
if existing, ok := s.shareCache[l.Hash]; ok && existing != nil {
|
||||
// update existing cached pointer fields
|
||||
existing.Source = l.Source
|
||||
existing.Path = l.Path
|
||||
} else {
|
||||
s.shareCache[l.Hash] = l
|
||||
}
|
||||
s.mu.Unlock()
|
||||
logger.Info("share updated", "hash", l.Hash, "fromPath", oldFullPath, "toPath", l.Path)
|
||||
updated++
|
||||
}
|
||||
if updated == 0 {
|
||||
logger.Warning("no matching shares to update for provided source/path")
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// Save wraps StorageBackend.Save
|
||||
func (s *Storage) Save(l *Link) error {
|
||||
return s.back.Save(l)
|
||||
if err := s.back.Save(l); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.shareCache[l.Hash] = l
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete wraps StorageBackend.Delete
|
||||
func (s *Storage) Delete(hash string) error {
|
||||
return s.back.Delete(hash)
|
||||
if err := s.back.Delete(hash); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
delete(s.shareCache, hash)
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush persists the current in-memory state of all shares to the backing store.
|
||||
// Call during graceful shutdown to ensure DB matches memory.
|
||||
func (s *Storage) Flush() error {
|
||||
s.mu.RLock()
|
||||
links := make([]*Link, 0, len(s.shareCache))
|
||||
for _, l := range s.shareCache {
|
||||
if l != nil {
|
||||
links = append(links, l)
|
||||
}
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
for _, l := range links {
|
||||
if err := s.back.Save(l); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterExpired removes expired links and deletes them from storage.
|
||||
@@ -122,6 +309,9 @@ func (s *Storage) filterExpired(links []*Link) ([]*Link, error) {
|
||||
for _, link := range links {
|
||||
if link.Expire != 0 && link.Expire <= time.Now().Unix() && !link.KeepAfterExpiration {
|
||||
_ = s.back.Delete(link.Hash)
|
||||
s.mu.Lock()
|
||||
delete(s.shareCache, link.Hash)
|
||||
s.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, link)
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewStorage(db *storm.DB) (*BoltStore, error) {
|
||||
}
|
||||
return &BoltStore{
|
||||
Users: userStore,
|
||||
Share: share.NewStorage(shareBackend{db: db}),
|
||||
Share: share.NewStorage(shareBackend{db: db}, userStore),
|
||||
Auth: authStore,
|
||||
Settings: settings.NewStorage(settingsBackend{db: db}),
|
||||
Access: access.NewStorage(db, userStore),
|
||||
|
||||
@@ -52,6 +52,16 @@ func (s shareBackend) GetPermanent(path, source string, id uint) (*share.Link, e
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
// GetBySourcePath returns all shares that exactly match Path and Source across users.
|
||||
func (s shareBackend) GetBySourcePath(path, source string) ([]*share.Link, error) {
|
||||
var v []*share.Link
|
||||
err := s.db.Select(q.Eq("Path", path), q.Eq("Source", source)).Find(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return nil, errors.ErrNotExist
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (s shareBackend) Gets(path, sourcePath string, id uint) ([]*share.Link, error) {
|
||||
var v []*share.Link
|
||||
_ = s.db.Select(q.Eq("Path", path), q.Eq("Source", sourcePath), q.Eq("UserID", id)).Find(&v)
|
||||
@@ -60,7 +70,7 @@ func (s shareBackend) Gets(path, sourcePath string, id uint) ([]*share.Link, err
|
||||
// through and filter out expired share
|
||||
for i := range v {
|
||||
if v[i].Expire < time.Now().Unix() && v[i].Expire != 0 {
|
||||
err = s.Delete(v[i].PasswordHash)
|
||||
err = s.Delete(v[i].Hash)
|
||||
if err != nil {
|
||||
logger.Errorf("expired share could not be deleted: %v", err.Error())
|
||||
}
|
||||
|
||||
+15
-7
@@ -1,6 +1,6 @@
|
||||
module github.com/gtsteffaniak/filebrowser/backend
|
||||
|
||||
go 1.24.2
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
@@ -24,13 +24,14 @@ require (
|
||||
golang.org/x/mod v0.27.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/time v0.11.0
|
||||
golang.org/x/time v0.12.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/4meepo/tagalign v1.4.2 // indirect
|
||||
github.com/Abirdcfly/dupword v0.1.3 // indirect
|
||||
github.com/Antonboom/errname v1.1.0 // indirect
|
||||
@@ -42,6 +43,7 @@ require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/air-verse/air v1.62.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.17.2 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||
@@ -52,6 +54,8 @@ require (
|
||||
github.com/ashanbrown/makezero v1.2.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bep/godartsass/v2 v2.5.0 // indirect
|
||||
github.com/bep/golibsass v1.2.0 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
||||
@@ -72,6 +76,7 @@ require (
|
||||
github.com/chavacava/garif v0.1.0 // indirect
|
||||
github.com/ckaznocha/intrange v0.3.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/creack/pty v1.1.24 // indirect
|
||||
github.com/curioswitch/go-reassign v0.3.0 // indirect
|
||||
github.com/daixiang0/gci v0.13.6 // indirect
|
||||
github.com/dave/dst v0.27.3 // indirect
|
||||
@@ -85,7 +90,7 @@ require (
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.6 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/ghostiam/protogetter v0.3.15 // indirect
|
||||
@@ -109,7 +114,8 @@ require (
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20250812090027-44f18bf34695 // indirect
|
||||
github.com/gohugoio/hugo v0.147.6 // indirect
|
||||
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.0 // indirect
|
||||
@@ -164,7 +170,7 @@ require (
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mgechev/revive v1.9.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/moricho/tparallel v0.3.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
@@ -200,7 +206,7 @@ require (
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sonatard/noctx v0.1.0 // indirect
|
||||
github.com/sourcegraph/go-diff v0.7.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cast v1.8.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
@@ -211,6 +217,7 @@ require (
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/tdakkota/asciicheck v0.4.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.1 // indirect
|
||||
github.com/tetafro/godot v1.5.1 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
|
||||
github.com/timonwong/loggercheck v0.11.0 // indirect
|
||||
@@ -230,7 +237,7 @@ require (
|
||||
go-simpler.org/musttag v0.13.1 // indirect
|
||||
go-simpler.org/sloglint v0.11.0 // indirect
|
||||
go.augendre.info/fatcontext v0.8.0 // indirect
|
||||
go.etcd.io/bbolt v1.4.2 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
@@ -252,6 +259,7 @@ require (
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/air-verse/air
|
||||
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
|
||||
github.com/swaggo/swag/cmd/swag
|
||||
)
|
||||
+119
-17
@@ -34,6 +34,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
|
||||
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
|
||||
@@ -45,6 +47,8 @@ github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+n
|
||||
github.com/Antonboom/nilnil v1.1.0/go.mod h1:b7sAlogQjFa1wV8jUW3o4PMzDVFLbTux+xnQdvzdcIE=
|
||||
github.com/Antonboom/testifylint v1.6.1 h1:6ZSytkFWatT8mwZlmRCHkWz1gPi+q6UBSbieji2Gj/o=
|
||||
github.com/Antonboom/testifylint v1.6.1/go.mod h1:k+nEkathI2NFjKO6HvwmSrbzUcQ6FAnbZV+ZRrnXPLI=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
@@ -63,6 +67,8 @@ github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsu
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/air-verse/air v1.62.0 h1:6CoXL4MAX9dc4xAzLfjMcDfbBoGmW5VjuuTV/1+bI+M=
|
||||
github.com/air-verse/air v1.62.0/go.mod h1:EO+jWuetL10tS9raffwg8WEV0t0KUeucRRaf9ii86dA=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
||||
@@ -84,6 +90,8 @@ github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQ
|
||||
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
|
||||
github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
|
||||
github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
|
||||
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
|
||||
github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY=
|
||||
@@ -98,6 +106,32 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
|
||||
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bep/gitmap v1.6.0 h1:sDuQMm9HoTL0LtlrfxjbjgAg2wHQd4nkMup2FInYzhA=
|
||||
github.com/bep/gitmap v1.6.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw=
|
||||
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
|
||||
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
|
||||
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
|
||||
github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU=
|
||||
github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
|
||||
github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
|
||||
github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5kujw=
|
||||
github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg=
|
||||
github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q=
|
||||
github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc=
|
||||
github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k=
|
||||
github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
|
||||
github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8=
|
||||
github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk=
|
||||
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
|
||||
github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
|
||||
github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE=
|
||||
github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro=
|
||||
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
|
||||
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
|
||||
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
|
||||
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
|
||||
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
||||
@@ -143,6 +177,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
|
||||
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||
@@ -150,6 +186,8 @@ github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmr
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
|
||||
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
|
||||
github.com/daixiang0/gci v0.13.6 h1:RKuEOSkGpSadkGbvZ6hJ4ddItT3cVZ9Vn9Rybk6xjl8=
|
||||
@@ -163,6 +201,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
|
||||
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
|
||||
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
|
||||
@@ -190,22 +230,29 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
|
||||
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
|
||||
github.com/evanw/esbuild v0.25.3 h1:4JKyUsm/nHDhpxis4IyWXAi8GiyTwG1WdEp6OhGVE8U=
|
||||
github.com/evanw/esbuild v0.25.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E=
|
||||
github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
|
||||
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
|
||||
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY=
|
||||
github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
|
||||
github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY=
|
||||
@@ -273,6 +320,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
|
||||
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
@@ -280,13 +329,29 @@ github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
|
||||
github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg=
|
||||
github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec=
|
||||
github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs=
|
||||
github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
|
||||
github.com/gohugoio/hugo v0.147.6 h1:rL4rnus/5qzj4+FoA+JMzsVvFJ2YZdVIH6pbuCB2P84=
|
||||
github.com/gohugoio/hugo v0.147.6/go.mod h1:Sb2COQPDPYG+tRSpePtzKytiuVDqkBivEhgIew1QbNo=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.3.0 h1:gj49kTR5Z4Hnm0ZaQrgPVazL3DUkppw+x6XhHCmh+Wk=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.3.0/go.mod h1:IMMj7xiUbLt1YNJ6m7AM4cnsX4cFnnfkleO/lBHGzUg=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1 h1:nUzXfRTszLliZuN0JTKeunXTRaiFX6ksaWP0puLLYAY=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1/go.mod h1:Wy8ThAA8p2/w1DY05vEzq6EIeI2mzDjvHsu7ULBVwog=
|
||||
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
|
||||
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
|
||||
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
|
||||
github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20250812090027-44f18bf34695 h1:W3KSdht88NFfM71hd6Y4j2uiW81/aoqjPrMVoV/ph1E=
|
||||
github.com/golang/geo v0.0.0-20250812090027-44f18bf34695/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
|
||||
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac h1:gBtvd74wZT02fkOyoG651Sx2imv5lKdVvDwpUjKPFnw=
|
||||
github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -386,6 +451,8 @@ github.com/gtsteffaniak/go-cache v0.0.0-20250521142451-edc77dfcb063 h1:CoLbeqTmM
|
||||
github.com/gtsteffaniak/go-cache v0.0.0-20250521142451-edc77dfcb063/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
|
||||
github.com/gtsteffaniak/go-logger v0.1.2 h1:0g6UxxNwU+Q+7IVRw+Yn7AuYsIK0ukiTgbzjTLm+Uy4=
|
||||
github.com/gtsteffaniak/go-logger v0.1.2/go.mod h1:U3ZkdAclcxXNMvoQLq+zDkqcTmdqhX3hTf+6AuTo6v0=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
@@ -404,6 +471,8 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
|
||||
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/jgautheron/goconst v1.8.1 h1:PPqCYp3K/xlOj5JmIe6O1Mj6r1DbkdbLtR3AJuZo414=
|
||||
@@ -450,6 +519,8 @@ github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=
|
||||
github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=
|
||||
github.com/kunwardeep/paralleltest v1.0.14 h1:wAkMoMeGX/kGfhQBPODT/BL8XhK23ol/nuQ3SwFaUw8=
|
||||
github.com/kunwardeep/paralleltest v1.0.14/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=
|
||||
github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
|
||||
github.com/ldez/exptostd v0.4.3 h1:Ag1aGiq2epGePuRJhez2mzOpZ8sI9Gimcb4Sb3+pk9Y=
|
||||
@@ -474,12 +545,16 @@ github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamh
|
||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
|
||||
github.com/manuelarte/funcorder v0.2.1 h1:7QJsw3qhljoZ5rH0xapIvjw31EcQeFbF31/7kQ/xS34=
|
||||
github.com/manuelarte/funcorder v0.2.1/go.mod h1:BQQ0yW57+PF9ZpjpeJDKOffEsQbxDFKW8F8zSMe/Zd0=
|
||||
github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI=
|
||||
github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE=
|
||||
github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04=
|
||||
github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc=
|
||||
github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
|
||||
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
|
||||
github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
|
||||
github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
@@ -497,27 +572,37 @@ github.com/mgechev/revive v1.9.0 h1:8LaA62XIKrb8lM6VsBSQ92slt/o92z5+hTw3CmrvSrM=
|
||||
github.com/mgechev/revive v1.9.0/go.mod h1:LAPq3+MgOf7GcL5PlWIkHb0PT7XH4NuC2LdWymhb9Mo=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
|
||||
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
|
||||
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
|
||||
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
|
||||
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
|
||||
github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
|
||||
github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
|
||||
github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=
|
||||
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
|
||||
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
|
||||
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
|
||||
github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4=
|
||||
github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
|
||||
@@ -531,10 +616,14 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -622,8 +711,8 @@ github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCp
|
||||
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
@@ -663,12 +752,20 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
|
||||
github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
|
||||
github.com/tdewolff/minify/v2 v2.23.5 h1:/P548KcpTkIOUvNg22zN83/GiaYSOIrbqtoue4I7kYM=
|
||||
github.com/tdewolff/minify/v2 v2.23.5/go.mod h1:2RI9tiIrzJU1Z5EasXEPaI1MqobRyxKHOOgrRkq5oEw=
|
||||
github.com/tdewolff/parse/v2 v2.8.1 h1:J5GSHru6o3jF1uLlEKVXkDxxcVx6yzOlIVIotK4w2po=
|
||||
github.com/tdewolff/parse/v2 v2.8.1/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
|
||||
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
|
||||
github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4=
|
||||
github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk=
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
|
||||
github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
|
||||
@@ -706,6 +803,10 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
|
||||
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
|
||||
gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
|
||||
go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
|
||||
@@ -717,8 +818,8 @@ go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkye
|
||||
go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk=
|
||||
go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -755,8 +856,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
|
||||
@@ -908,7 +1009,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -947,8 +1047,8 @@ golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
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-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -1129,6 +1229,8 @@ mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg=
|
||||
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8=
|
||||
mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4/go.mod h1:rthT7OuvRbaGcd5ginj6dA2oLE7YNlta9qhBNNdCaLE=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
|
||||
@@ -23,7 +23,7 @@ type GroupListResponse struct {
|
||||
// @Param path query string false "Index path (e.g. /secret)"
|
||||
// @Param user query string false "Username to filter rules for"
|
||||
// @Param group query string false "Group name to filter rules for"
|
||||
// @Success 200 {object} object "Varies based on query. Can be access.FrontendAccessRule, []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule"
|
||||
// @Success 200 {object} object "Varies based on query. Can be access.FrontendAccessRule (when source and path specified), []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule"
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/access [get]
|
||||
|
||||
@@ -76,11 +76,12 @@ func setupProxyUser(r *http.Request, data *requestContext, proxyUser string) (*u
|
||||
return nil, err
|
||||
}
|
||||
if config.Auth.Methods.ProxyAuth.CreateUser {
|
||||
err = storage.CreateUser(users.User{
|
||||
user := users.User{
|
||||
LoginMethod: users.LoginMethodProxy,
|
||||
Username: proxyUser,
|
||||
Permissions: settings.Config.UserDefaults.Permissions,
|
||||
})
|
||||
}
|
||||
settings.ApplyUserDefaults(&user)
|
||||
err = storage.CreateUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+47
-10
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
@@ -23,9 +24,6 @@ import (
|
||||
//go:embed embed/*
|
||||
var assets embed.FS
|
||||
|
||||
// Boolean flag to determine whether to use the embedded FS or not
|
||||
var embeddedFS = os.Getenv("FILEBROWSER_NO_EMBEDED") != "true"
|
||||
|
||||
// Custom dirFS to handle both embedded and non-embedded file systems
|
||||
type dirFS struct {
|
||||
http.Dir
|
||||
@@ -45,7 +43,18 @@ var (
|
||||
func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete chan struct{}) {
|
||||
store = storage
|
||||
config = &settings.Config
|
||||
var err error
|
||||
|
||||
// Dev mode enables development features like template hot-reloading
|
||||
devMode := os.Getenv("FILEBROWSER_DEVMODE") == "true"
|
||||
_, err := os.Stat("http/dist")
|
||||
// In dev mode, always use filesystem assets. Otherwise, check if http/dist exists
|
||||
var embeddedFS bool
|
||||
if devMode {
|
||||
embeddedFS = false
|
||||
} else {
|
||||
embeddedFS = os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// --- START: ADD THIS DECRYPTION LOGIC ---
|
||||
if embeddedFS {
|
||||
// Embedded mode: Serve files from the embedded assets
|
||||
@@ -57,8 +66,20 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
assetFs = dirFS{Dir: http.Dir("http/dist")}
|
||||
}
|
||||
|
||||
// In development mode, we want to reload the templates on each request.
|
||||
// In production (embedded), we parse them once.
|
||||
templates := template.New("").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) (string, error) {
|
||||
a, err := json.Marshal(v)
|
||||
return string(a), err
|
||||
},
|
||||
})
|
||||
if !devMode {
|
||||
templates = template.Must(templates.ParseFS(assetFs, "public/index.html"))
|
||||
}
|
||||
templateRenderer = &TemplateRenderer{
|
||||
templates: template.Must(template.ParseFS(assetFs, "public/index.html")),
|
||||
templates: templates,
|
||||
devMode: devMode,
|
||||
}
|
||||
|
||||
router := http.NewServeMux()
|
||||
@@ -111,13 +132,15 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
// NEW PUBLIC ROUTES - All publicly accessible endpoints
|
||||
// Share management routes (require permission but are publicly accessible)
|
||||
publicRoutes.HandleFunc("GET /shares", withPermShare(shareListHandler))
|
||||
publicRoutes.HandleFunc("GET /share/direct", withPermShare(shareDirectDownloadHandler))
|
||||
publicRoutes.HandleFunc("GET /share", withPermShare(shareGetHandler))
|
||||
publicRoutes.HandleFunc("POST /share", withPermShare(sharePostHandler))
|
||||
publicRoutes.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler))
|
||||
// Public API routes (hash-based authentication)
|
||||
publicAPI.HandleFunc("GET /raw", withHashFile(publicRawHandler))
|
||||
publicAPI.HandleFunc("GET /preview", withHashFile(publicPreviewHandler))
|
||||
publicAPI.HandleFunc("GET /shared", withHashFile(publicShareHandler))
|
||||
publicAPI.HandleFunc("GET /resources", withHashFile(publicShareHandler))
|
||||
publicAPI.HandleFunc("GET /users", withUser(userGetHandler))
|
||||
// Settings routes
|
||||
api.HandleFunc("GET /settings", withAdmin(settingsGetHandler))
|
||||
|
||||
@@ -202,11 +225,11 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
// New frontend share route handler - handle share page and any subpaths
|
||||
publicRoutes.HandleFunc("GET /share/", withOrWithoutUser(indexHandler))
|
||||
|
||||
// Public static assets (needed for shared pages to load CSS/JS/images)
|
||||
publicRoutes.HandleFunc("GET /static/", staticFilesHandler)
|
||||
|
||||
// Static and index file handlers
|
||||
router.HandleFunc(fmt.Sprintf("GET %vstatic/", config.Server.BaseURL), staticFilesHandler)
|
||||
staticPrefix := config.Server.BaseURL + "static/"
|
||||
router.Handle(staticPrefix, http.StripPrefix(staticPrefix, http.HandlerFunc(staticFilesHandler)))
|
||||
publicRoutes.Handle("GET /static/", http.StripPrefix("/static/", http.HandlerFunc(staticFilesHandler)))
|
||||
|
||||
router.HandleFunc(config.Server.BaseURL, withOrWithoutUser(indexHandler))
|
||||
router.HandleFunc(fmt.Sprintf("GET %vhealth", config.Server.BaseURL), healthHandler)
|
||||
router.Handle(fmt.Sprintf("%vswagger/", config.Server.BaseURL), withUser(swaggerHandler))
|
||||
@@ -279,6 +302,20 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
<-ctx.Done()
|
||||
logger.Info("Shutting down HTTP server...")
|
||||
|
||||
// Persist in-memory state before shutting down the HTTP server
|
||||
if store != nil {
|
||||
if store.Share != nil {
|
||||
if err := store.Share.Flush(); err != nil {
|
||||
logger.Errorf("Failed to flush share storage: %v", err)
|
||||
}
|
||||
}
|
||||
if store.Access != nil {
|
||||
if err := store.Access.Flush(); err != nil {
|
||||
logger.Errorf("Failed to flush access storage: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Graceful shutdown with a timeout - 30 seconds, in case downloads are happening
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
+43
-19
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -28,6 +27,7 @@ type requestContext struct {
|
||||
fileInfo iteminfo.ExtendedFileInfo
|
||||
token string
|
||||
share *share.Link
|
||||
shareValid bool
|
||||
ctx context.Context
|
||||
MaxBandwidth int
|
||||
}
|
||||
@@ -47,15 +47,11 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, data *requestContex
|
||||
func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
return withOrWithoutUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
||||
hash := r.URL.Query().Get("hash")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
path := r.URL.Query().Get("path")
|
||||
// Get the file link by hash
|
||||
link, err := store.Share.GetByHash(hash)
|
||||
if err != nil {
|
||||
data.share = &share.Link{}
|
||||
return http.StatusNotFound, fmt.Errorf("share hash not found")
|
||||
}
|
||||
if link.DisableAnonymous && data.user.Username == "anonymous" {
|
||||
@@ -79,19 +75,31 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
if !ok {
|
||||
return http.StatusNotFound, fmt.Errorf("source not found")
|
||||
}
|
||||
if source.Config.Private {
|
||||
return http.StatusForbidden, fmt.Errorf("the target source is private")
|
||||
}
|
||||
// Get file information with options
|
||||
getContent := r.URL.Query().Get("content") == "true"
|
||||
if link.DisableFileViewer || link.Downloads >= link.DownloadsLimit {
|
||||
getContent = false
|
||||
}
|
||||
file, err := FileInfoFasterFunc(iteminfo.FileOptions{
|
||||
Path: utils.JoinPathAsUnix(link.Path, path),
|
||||
Source: source.Name,
|
||||
Source: link.Source,
|
||||
Modify: false,
|
||||
Expand: true,
|
||||
Content: r.URL.Query().Get("content") == "true",
|
||||
Content: getContent,
|
||||
})
|
||||
file.Token = link.Token
|
||||
if err != nil {
|
||||
logger.Errorf("error fetching file info for share. hash=%v path=%v error=%v", hash, path, err)
|
||||
return errToStatus(err), fmt.Errorf("error fetching share from server")
|
||||
}
|
||||
if getContent && file.Content != "" {
|
||||
link.Mu.Lock()
|
||||
link.Downloads++
|
||||
link.Mu.Unlock()
|
||||
}
|
||||
file.Path = "/" + strings.TrimPrefix(strings.TrimPrefix(file.Path, link.Path), "/")
|
||||
// Set the file info in the `data` object
|
||||
data.fileInfo = file
|
||||
@@ -117,9 +125,14 @@ func withAdminHelper(fn handleFunc) handleFunc {
|
||||
func withOrWithoutUserHelper(fn handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
||||
var link *share.Link
|
||||
var isShareRequest bool
|
||||
var shareHash string
|
||||
|
||||
hash := r.URL.Query().Get("hash")
|
||||
if hash != "" {
|
||||
// Get the file link by hash
|
||||
isShareRequest = true
|
||||
shareHash = hash
|
||||
link, _ = store.Share.GetByHash(hash)
|
||||
} else {
|
||||
prefix := config.Server.BaseURL + "public/share/"
|
||||
@@ -131,6 +144,8 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc {
|
||||
remaining = remaining[:idx]
|
||||
}
|
||||
if remaining != "" {
|
||||
isShareRequest = true
|
||||
shareHash = remaining
|
||||
var err error
|
||||
link, err = store.Share.GetByHash(remaining)
|
||||
if err != nil {
|
||||
@@ -141,13 +156,24 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a share request, always create a share context (even if invalid)
|
||||
if isShareRequest {
|
||||
if link != nil {
|
||||
data.share = link
|
||||
data.shareValid = true
|
||||
} else {
|
||||
// Create an empty share with just the hash for invalid shares
|
||||
data.share = &share.Link{Hash: shareHash}
|
||||
data.shareValid = false
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate user first
|
||||
status, err := withUserHelper(nil)(w, r, data)
|
||||
if err == nil && status < 400 {
|
||||
if link != nil {
|
||||
data.share = link
|
||||
if data.share != nil && data.shareValid {
|
||||
if data.user != nil {
|
||||
data.user.CustomTheme = link.ShareTheme
|
||||
data.user.CustomTheme = data.share.ShareTheme
|
||||
}
|
||||
}
|
||||
return fn(w, r, data)
|
||||
@@ -158,14 +184,13 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc {
|
||||
// If user authentication failed, call the handler without user context
|
||||
// Clear any user data that might have been partially set
|
||||
data.token = ""
|
||||
if link != nil {
|
||||
data.share = link
|
||||
data.user.CustomTheme = link.ShareTheme
|
||||
if data.share != nil && data.shareValid {
|
||||
data.user.CustomTheme = data.share.ShareTheme
|
||||
}
|
||||
// Call the handler function without user context
|
||||
return fn(w, r, data)
|
||||
}
|
||||
return status, fmt.Errorf("could not authenticate share request")
|
||||
return status, fmt.Errorf("could not authenticate request")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,11 +292,10 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
// Check if the token is about to expire and send a header to renew it
|
||||
if tk.Expires < time.Now().Add(time.Hour).Unix() {
|
||||
w.Header().Add("X-Renew-Token", "true")
|
||||
}
|
||||
|
||||
// Retrieve the user from the store and store it in the context
|
||||
} // Retrieve the user from the store and store it in the context
|
||||
data.user, err = store.Users.Get(tk.BelongsTo)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get user with ID %v: %v", tk.BelongsTo, err)
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
setUserInResponseWriter(w, data.user)
|
||||
|
||||
@@ -150,8 +150,10 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||
{
|
||||
name: "Public share, no auth required",
|
||||
share: &share.Link{
|
||||
Hash: "public_hash",
|
||||
Source: "/srv",
|
||||
Hash: "public_hash",
|
||||
CommonShare: share.CommonShare{
|
||||
Source: "/srv",
|
||||
},
|
||||
},
|
||||
expectedStatusCode: 0, // zero means 200 on helpers
|
||||
},
|
||||
@@ -162,7 +164,9 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||
UserID: 1,
|
||||
PasswordHash: passwordBcrypt,
|
||||
Token: "some_random_token",
|
||||
Source: "/srv",
|
||||
CommonShare: share.CommonShare{
|
||||
Source: "/srv",
|
||||
},
|
||||
},
|
||||
extraHeaders: map[string]string{
|
||||
"X-SHARE-PASSWORD": "password",
|
||||
@@ -186,7 +190,9 @@ func TestPublicShareHandlerAuthentication(t *testing.T) {
|
||||
UserID: 1,
|
||||
PasswordHash: passwordBcrypt,
|
||||
Token: "123",
|
||||
Source: "/srv",
|
||||
CommonShare: share.CommonShare{
|
||||
Source: "/srv",
|
||||
},
|
||||
},
|
||||
token: "123",
|
||||
expectedStatusCode: 0, // zero means 200 on helpers
|
||||
|
||||
@@ -292,15 +292,15 @@ func loginWithOidcUser(w http.ResponseWriter, r *http.Request, username string,
|
||||
if config.Auth.Methods.OidcAuth.AdminGroup == "" {
|
||||
isAdmin = config.UserDefaults.Permissions.Admin
|
||||
}
|
||||
newUser := users.User{
|
||||
LoginMethod: users.LoginMethodOidc,
|
||||
user = &users.User{
|
||||
Username: username,
|
||||
Permissions: settings.Config.UserDefaults.Permissions,
|
||||
LoginMethod: users.LoginMethodOidc,
|
||||
}
|
||||
settings.ApplyUserDefaults(user)
|
||||
if isAdmin {
|
||||
newUser.Permissions.Admin = true
|
||||
user.Permissions.Admin = true
|
||||
}
|
||||
err = storage.CreateUser(newUser)
|
||||
err = storage.CreateUser(*user)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
+22
-8
@@ -19,7 +19,7 @@ import (
|
||||
// @Tags Resources
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param files query string true "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)"
|
||||
// @Param files query string false "if specified, only the files in the list will be downloaded. eg. files=/file1||/folder/file2"
|
||||
// @Param inline query bool false "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'."
|
||||
// @Param algo query string false "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'."
|
||||
// @Success 200 {file} file "Raw file or directory content, or archive for multiple files"
|
||||
@@ -29,17 +29,14 @@ import (
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /public/dl [get]
|
||||
func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
if d.share.Downloads > d.share.DownloadsLimit {
|
||||
if d.share.DownloadsLimit > 0 && d.share.Downloads >= d.share.DownloadsLimit {
|
||||
return http.StatusForbidden, fmt.Errorf("share downloads limit reached")
|
||||
}
|
||||
d.share.Mu.Lock()
|
||||
d.share.Downloads++
|
||||
d.share.Mu.Unlock()
|
||||
|
||||
encodedFiles := r.URL.Query().Get("files")
|
||||
if encodedFiles == "" {
|
||||
return http.StatusBadRequest, fmt.Errorf("no file list provided, should be in the format '/path1||/path2||/path3'")
|
||||
}
|
||||
|
||||
// Decode the URL-encoded path
|
||||
f, err := url.QueryUnescape(encodedFiles)
|
||||
if err != nil {
|
||||
@@ -48,8 +45,25 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
|
||||
fileList := []string{}
|
||||
for _, file := range strings.Split(f, "||") {
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
fileList = append(fileList, d.fileInfo.Source+"::"+filePath)
|
||||
// Check if file already contains source prefix (source::path format)
|
||||
if strings.Contains(file, "::") {
|
||||
splitFile := strings.SplitN(file, "::", 2)
|
||||
if len(splitFile) == 2 {
|
||||
source := splitFile[0]
|
||||
path := splitFile[1]
|
||||
// Join the share path with the requested path
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, path)
|
||||
fileList = append(fileList, source+"::"+filePath)
|
||||
} else {
|
||||
// Fallback: treat as plain path
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
fileList = append(fileList, d.fileInfo.Source+"::"+filePath)
|
||||
}
|
||||
} else {
|
||||
// Plain path without source prefix
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
fileList = append(fileList, d.fileInfo.Source+"::"+filePath)
|
||||
}
|
||||
}
|
||||
|
||||
var status int
|
||||
|
||||
+16
-3
@@ -104,6 +104,7 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
}
|
||||
path = utils.JoinPathAsUnix(userScope, path)
|
||||
}
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
@@ -173,15 +174,20 @@ func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *z
|
||||
}
|
||||
return addSingleFile(filePath, relPath, zipWriter, tarWriter)
|
||||
})
|
||||
} else {
|
||||
// For a single file, use the base name as the archive path
|
||||
return addSingleFile(realPath, baseName, zipWriter, tarWriter)
|
||||
}
|
||||
|
||||
// For a single file, use the base name as the archive path
|
||||
return addSingleFile(realPath, baseName, zipWriter, tarWriter)
|
||||
}
|
||||
|
||||
func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWriter *tar.Writer) error {
|
||||
file, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
// If we get "is a directory" error, this is likely a symlink to a directory
|
||||
// that wasn't properly detected. Skip it gracefully.
|
||||
if strings.Contains(err.Error(), "is a directory") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -191,6 +197,11 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
return err
|
||||
}
|
||||
|
||||
// Double-check if this is actually a directory (in case of symlinks)
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tarWriter != nil {
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
@@ -240,6 +251,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
||||
}
|
||||
firstFilePath = utils.JoinPathAsUnix(userscope, firstFilePath)
|
||||
}
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
idx := indexing.GetIndex(firstFileSource)
|
||||
if idx == nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", firstFileSource)
|
||||
@@ -395,6 +407,7 @@ func computeArchiveSize(fileList []string, d *requestContext) (int64, error) {
|
||||
}
|
||||
path = utils.JoinPathAsUnix(userScope, path)
|
||||
}
|
||||
// For shares, the path is already correctly resolved by publicRawHandler
|
||||
realPath, isDir, err := idx.GetRealPath(path)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
|
||||
@@ -85,6 +85,10 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
scopePath := utils.JoinPathAsUnix(userscope, path)
|
||||
getContent := r.URL.Query().Get("content") == "true"
|
||||
if d.share != nil && d.share.DisableFileViewer {
|
||||
getContent = false
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
|
||||
Access: store.Access,
|
||||
Username: d.user.Username,
|
||||
@@ -92,7 +96,7 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Content: r.URL.Query().Get("content") == "true",
|
||||
Content: getContent,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@@ -568,7 +572,7 @@ func patchAction(ctx context.Context, params patchActionParams) error {
|
||||
|
||||
// delete thumbnails
|
||||
preview.DelThumbs(ctx, fileInfo)
|
||||
return files.MoveResource(params.isSrcDir, params.isDstDir, params.srcIndex, params.dstIndex, params.src, params.dst)
|
||||
return files.MoveResource(params.isSrcDir, params.isDstDir, params.srcIndex, params.dstIndex, params.src, params.dst, store.Share)
|
||||
default:
|
||||
return fmt.Errorf("unsupported action %s: %w", params.action, errors.ErrInvalidRequestParams)
|
||||
}
|
||||
|
||||
+283
-52
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -18,8 +17,35 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
)
|
||||
|
||||
// ShareResponse represents a share with computed username field and download URL
|
||||
type ShareResponse struct {
|
||||
*share.Link
|
||||
Username string `json:"username,omitempty"`
|
||||
DownloadURL string `json:"downloadURL,omitempty"`
|
||||
}
|
||||
|
||||
// convertToShareResponse converts shares to response format with usernames
|
||||
func convertToShareResponse(r *http.Request, shares []*share.Link) ([]*ShareResponse, error) {
|
||||
responses := make([]*ShareResponse, len(shares))
|
||||
for i, s := range shares {
|
||||
user, err := store.Users.Get(s.UserID)
|
||||
username := ""
|
||||
if err == nil {
|
||||
username = user.Username
|
||||
}
|
||||
responses[i] = &ShareResponse{
|
||||
Link: s,
|
||||
Username: username,
|
||||
DownloadURL: getDownloadURL(r, s.Hash),
|
||||
}
|
||||
}
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
// shareListHandler returns a list of all share links.
|
||||
// @Summary List share links
|
||||
// @Description Returns a list of share links for the current user, or all links if the user is an admin.
|
||||
@@ -37,18 +63,15 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
} else {
|
||||
shares, err = store.Share.FindByUserID(d.user.ID)
|
||||
}
|
||||
|
||||
if err != nil && err != errors.ErrNotExist {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
shares = utils.NonNilSlice(shares)
|
||||
sort.Slice(shares, func(i, j int) bool {
|
||||
if shares[i].UserID != shares[j].UserID {
|
||||
return shares[i].UserID < shares[j].UserID
|
||||
}
|
||||
return shares[i].Expire < shares[j].Expire
|
||||
})
|
||||
return renderJSON(w, r, shares)
|
||||
sharesWithUsernames, err := convertToShareResponse(r, shares)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return renderJSON(w, r, sharesWithUsernames)
|
||||
}
|
||||
|
||||
// shareGetsHandler retrieves share links for a specific resource path.
|
||||
@@ -64,33 +87,34 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
// @Router /api/share [get]
|
||||
func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
sourceName := r.URL.Query().Get("source")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
sourcePath, ok := config.Server.NameToSource[source]
|
||||
sourceInfo, ok := config.Server.NameToSource[sourceName] // backend source is path
|
||||
if !ok {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", source)
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", sourceName)
|
||||
}
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, sourceName)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
scopePath := utils.JoinPathAsUnix(userscope, path)
|
||||
s, err := store.Share.Gets(scopePath, sourcePath.Path, d.user.ID)
|
||||
s, err := store.Share.Gets(scopePath, sourceInfo.Path, d.user.ID)
|
||||
if err == errors.ErrNotExist || len(s) == 0 {
|
||||
return renderJSON(w, r, []*share.Link{})
|
||||
}
|
||||
// Overwrite the Source field with the source name from the query for each link
|
||||
for _, link := range s {
|
||||
link.Source = source
|
||||
return renderJSON(w, r, []*ShareResponse{})
|
||||
}
|
||||
// DownloadURL will be set in convertToShareResponse
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
|
||||
}
|
||||
return renderJSON(w, r, s)
|
||||
sharesWithUsernames, err := convertToShareResponse(r, s)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return renderJSON(w, r, sharesWithUsernames)
|
||||
}
|
||||
|
||||
// shareDeleteHandler deletes a specific share link by its hash.
|
||||
@@ -125,12 +149,11 @@ func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// @Tags Shares
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "Source Path of the files to share"
|
||||
// @Param source query string true "Source name of the files to share"
|
||||
// @Param body body share.CreateBody true "Share creation parameters"
|
||||
// @Success 200 {object} share.Link "Created share link"
|
||||
// @Failure 400 {object} map[string]string "Bad request - failed to decode body"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/shares [post]
|
||||
// @Router /api/share [post]
|
||||
func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
var s *share.Link
|
||||
var err error
|
||||
@@ -192,11 +215,36 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
s.Expire = expire
|
||||
s.PasswordHash = stringHash
|
||||
s.Token = token
|
||||
// Preserve immutable fields for updates. Path and Source should not change on edits.
|
||||
// If the request attempts to provide empty values (or any values) for these,
|
||||
// keep the existing ones from the stored share.
|
||||
body.Path = s.Path
|
||||
body.Source = s.Source
|
||||
s.CommonShare = body.CommonShare
|
||||
if err = store.Share.Save(s); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
return renderJSON(w, r, s)
|
||||
// Convert to ShareResponse format with username
|
||||
var user *users.User
|
||||
user, err = store.Users.Get(s.UserID)
|
||||
username := ""
|
||||
if err == nil {
|
||||
username = user.Username
|
||||
}
|
||||
response := &ShareResponse{
|
||||
Link: s,
|
||||
Username: username,
|
||||
}
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
|
||||
source, ok := config.Server.NameToSource[body.Source]
|
||||
if !ok {
|
||||
return http.StatusForbidden, fmt.Errorf("source with name not found: %s", body.Source)
|
||||
}
|
||||
|
||||
if source.Config.Private {
|
||||
return http.StatusForbidden, fmt.Errorf("the target source is private, sharing is not permitted")
|
||||
}
|
||||
|
||||
// create a new share link
|
||||
@@ -204,37 +252,30 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
// validate source path exists
|
||||
idx := indexing.GetIndex(source.Name)
|
||||
if idx == nil {
|
||||
return http.StatusForbidden, fmt.Errorf("source with name not found: %s", body.Source)
|
||||
}
|
||||
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
sourceName := r.URL.Query().Get("source")
|
||||
source, ok := config.Server.NameToSource[sourceName]
|
||||
if !ok {
|
||||
// try to find source by path
|
||||
for _, s := range config.Server.Sources {
|
||||
if s.Path == sourceName {
|
||||
source = s
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
// validate path exists as file or folder
|
||||
_, exists := idx.GetReducedMetadata(body.Path, true) // true to check if it exists
|
||||
if !exists {
|
||||
// could be a file instead
|
||||
_, exists := idx.GetReducedMetadata(utils.GetParentDirectoryPath(body.Path), true)
|
||||
if !exists {
|
||||
return http.StatusForbidden, fmt.Errorf("path not found: %s", body.Path)
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return http.StatusForbidden, fmt.Errorf("source with name not found")
|
||||
}
|
||||
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source.Name)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
scopePath := utils.JoinPathAsUnix(userscope, path)
|
||||
scopePath := utils.JoinPathAsUnix(userscope, body.Path)
|
||||
body.Path = scopePath
|
||||
body.Source = source.Path // backend source is path
|
||||
s = &share.Link{
|
||||
Path: scopePath,
|
||||
Source: source.Path, // path instead to persist accoss name change
|
||||
Expire: expire,
|
||||
UserID: d.user.ID,
|
||||
Hash: secure_hash,
|
||||
@@ -243,16 +284,206 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
CommonShare: body.CommonShare,
|
||||
}
|
||||
|
||||
if err := store.Share.Save(s); err != nil {
|
||||
if err = store.Share.Save(s); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
// Convert to ShareResponse format with username
|
||||
var user *users.User
|
||||
user, err = store.Users.Get(s.UserID)
|
||||
username := ""
|
||||
if err == nil {
|
||||
username = user.Username
|
||||
}
|
||||
response := &ShareResponse{
|
||||
Link: s,
|
||||
Username: username,
|
||||
}
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
|
||||
// DirectDownloadResponse represents the response for direct download endpoint
|
||||
type DirectDownloadResponse struct {
|
||||
Status string `json:"status"`
|
||||
Hash string `json:"hash"`
|
||||
DownloadURL string `json:"url"`
|
||||
}
|
||||
|
||||
// shareDirectDownloadHandler creates a direct download link for files only.
|
||||
// @Summary Create direct download link
|
||||
// @Description Creates a direct download link for a specific file with configurable duration, download count, and speed limits. If a share already exists with matching parameters, the existing share will be reused.
|
||||
// @Tags Shares
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "File path to create download link for"
|
||||
// @Param source query string true "Source name for the file"
|
||||
// @Param duration query string false "Duration in minutes for link validity (default: 60)"
|
||||
// @Param count query string false "Maximum number of downloads allowed (default: unlimited)"
|
||||
// @Param speed query string false "Download speed limit in kbps (default: unlimited)"
|
||||
// @Success 201 {object} DirectDownloadResponse "Direct download link created"
|
||||
// @Failure 400 {object} map[string]string "Bad request - invalid parameters or path is not a file"
|
||||
// @Failure 403 {object} map[string]string "Forbidden - access denied"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /public/share/direct [get]
|
||||
func shareDirectDownloadHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// Extract query parameters
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
duration := r.URL.Query().Get("duration")
|
||||
downloadCountStr := r.URL.Query().Get("count")
|
||||
downloadSpeedStr := r.URL.Query().Get("speed")
|
||||
|
||||
// Validate required parameters
|
||||
if encodedPath == "" || source == "" {
|
||||
return http.StatusBadRequest, fmt.Errorf("path and source are required")
|
||||
}
|
||||
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
// Validate source exists
|
||||
sourceInfo, ok := config.Server.NameToSource[source]
|
||||
if !ok {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", source)
|
||||
}
|
||||
|
||||
// Get user scope for this source
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
|
||||
// Validate the path exists and is a file (not a folder)
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
return http.StatusForbidden, fmt.Errorf("source with name not found: %s", source)
|
||||
}
|
||||
|
||||
metadata, exists := idx.GetReducedMetadata(path, false)
|
||||
if !exists {
|
||||
return http.StatusBadRequest, fmt.Errorf("path is either not a file or not found: %s", path)
|
||||
}
|
||||
|
||||
// Check if it's a file (not a directory)
|
||||
if metadata.Type == "directory" {
|
||||
return http.StatusBadRequest, fmt.Errorf("path must be a file, not a directory: %s", path)
|
||||
}
|
||||
|
||||
// Set default duration to 60 minutes if not provided
|
||||
if duration == "" {
|
||||
duration = "60"
|
||||
}
|
||||
|
||||
// Parse download count
|
||||
var downloadCount int
|
||||
if downloadCountStr != "" {
|
||||
downloadCount, err = strconv.Atoi(downloadCountStr)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid downloadCount: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse download speed (in bytes per second)
|
||||
var downloadSpeed int
|
||||
if downloadSpeedStr != "" {
|
||||
downloadSpeed, err = strconv.Atoi(downloadSpeedStr)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid downloadSpeed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate expiration time
|
||||
durationNum, err := strconv.Atoi(duration)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid duration: %v", err)
|
||||
}
|
||||
expire := time.Now().Add(time.Minute * time.Duration(durationNum)).Unix()
|
||||
|
||||
// Generate secure hash for the share
|
||||
secureHash, err := generateShortUUID()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Overwrite the Source field with the source name from the query for each link
|
||||
s.Source = sourceName
|
||||
if body.Hash != "" {
|
||||
return renderJSON(w, r, s)
|
||||
// Create the scope path
|
||||
scopePath := utils.JoinPathAsUnix(userscope, path)
|
||||
|
||||
// Check if an existing share already matches these parameters
|
||||
existingShares, err := store.Share.Gets(scopePath, sourceInfo.Path, d.user.ID)
|
||||
if err == nil && len(existingShares) > 0 {
|
||||
// Look for a share that matches our parameters
|
||||
for _, existing := range existingShares {
|
||||
if existing.DownloadsLimit == downloadCount &&
|
||||
existing.MaxBandwidth == downloadSpeed &&
|
||||
existing.QuickDownload &&
|
||||
(existing.Expire == 0 || existing.Expire >= expire) { // Existing expires later or never
|
||||
|
||||
response := DirectDownloadResponse{
|
||||
Status: "201",
|
||||
Hash: existing.Hash,
|
||||
DownloadURL: getDownloadURL(r, existing.Hash),
|
||||
}
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
return renderJSON(w, r, s)
|
||||
|
||||
// No matching existing share found, create a new one
|
||||
shareLink := &share.Link{
|
||||
Expire: expire,
|
||||
UserID: d.user.ID,
|
||||
Hash: secureHash,
|
||||
CommonShare: share.CommonShare{
|
||||
Path: scopePath,
|
||||
Source: idx.Path,
|
||||
DownloadsLimit: downloadCount,
|
||||
MaxBandwidth: downloadSpeed,
|
||||
QuickDownload: true, // Enable quick download for direct downloads
|
||||
},
|
||||
}
|
||||
|
||||
// Save the share
|
||||
if err := store.Share.Save(shareLink); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
// Return response
|
||||
response := DirectDownloadResponse{
|
||||
Status: "200",
|
||||
Hash: secureHash,
|
||||
DownloadURL: getDownloadURL(r, secureHash),
|
||||
}
|
||||
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
|
||||
func getDownloadURL(r *http.Request, hash string) string {
|
||||
var downloadURL string
|
||||
if config.Server.ExternalUrl != "" {
|
||||
downloadURL = fmt.Sprintf("%s%spublic/api/raw?hash=%s", config.Server.ExternalUrl, config.Server.BaseURL, hash)
|
||||
} else {
|
||||
// Prefer X-Forwarded-Host for proxy support
|
||||
var host string
|
||||
var scheme string
|
||||
|
||||
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
|
||||
host = forwardedHost
|
||||
// Use X-Forwarded-Proto if available, otherwise default to https for proxied requests
|
||||
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
|
||||
scheme = forwardedProto
|
||||
} else {
|
||||
scheme = "https"
|
||||
}
|
||||
} else {
|
||||
// Fallback to simple approach
|
||||
host = r.Host
|
||||
scheme = getScheme(r)
|
||||
}
|
||||
downloadURL = fmt.Sprintf("%s://%s%spublic/api/raw?hash=%s", scheme, host, config.Server.BaseURL, hash)
|
||||
}
|
||||
return downloadURL
|
||||
}
|
||||
|
||||
func getSharePasswordHash(body share.CreateBody) (data []byte, statuscode int, err error) {
|
||||
|
||||
+68
-36
@@ -17,10 +17,19 @@ var templateRenderer *TemplateRenderer
|
||||
|
||||
type TemplateRenderer struct {
|
||||
templates *template.Template
|
||||
devMode bool
|
||||
}
|
||||
|
||||
// Render renders a template document with headers and data
|
||||
func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data interface{}) error {
|
||||
// If in dev mode, reload templates on every render.
|
||||
if t.devMode {
|
||||
var err error
|
||||
t.templates, err = t.templates.ParseFS(assetFs, "public/index.html")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reloading template: %w", err)
|
||||
}
|
||||
}
|
||||
// Set headers
|
||||
w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
@@ -45,7 +54,9 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
}
|
||||
|
||||
defaultThemeColor := "#455a64"
|
||||
staticURL := config.Server.BaseURL + "static"
|
||||
publicStaticURL := config.Server.BaseURL + "public/static"
|
||||
data := map[string]interface{}{
|
||||
"title": config.Frontend.Name,
|
||||
"customCSS": config.Frontend.Styling.CustomCSS,
|
||||
@@ -55,45 +66,67 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"staticURL": staticURL,
|
||||
"baseURL": config.Server.BaseURL,
|
||||
"favicon": staticURL + "/img/icons/favicon-256x256.png",
|
||||
"color": "var(--primaryColor)",
|
||||
"color": defaultThemeColor,
|
||||
"winIcon": staticURL + "/img/icons/mstile-144x144.png",
|
||||
"appIcon": staticURL + "/img/icons/android-chrome-256x256.png",
|
||||
"description": "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server",
|
||||
}
|
||||
shareOverrides := map[string]interface{}{
|
||||
"banner": "",
|
||||
"title": "",
|
||||
"quickDownload": false,
|
||||
"description": "",
|
||||
"themeColor": "",
|
||||
"disableThumbnails": false,
|
||||
"viewMode": "list",
|
||||
"disableFileViewer": false,
|
||||
shareProps := map[string]interface{}{
|
||||
"isShare": false,
|
||||
"isValid": false,
|
||||
"banner": "",
|
||||
"title": "",
|
||||
"quickDownload": false,
|
||||
"description": "",
|
||||
"themeColor": "",
|
||||
"disableThumbnails": false,
|
||||
"viewMode": "list",
|
||||
"disableFileViewer": false,
|
||||
"disableShareCard": false,
|
||||
"disableSidebar": false,
|
||||
"isPasswordProtected": false,
|
||||
"hash": "",
|
||||
}
|
||||
disableNavButtons := settings.Config.Frontend.DisableNavButtons
|
||||
if d.share != nil {
|
||||
disableNavButtons = disableNavButtons || d.share.HideNavButtons
|
||||
shareOverrides["viewMode"] = d.share.ViewMode
|
||||
shareOverrides["banner"] = d.share.Banner
|
||||
shareOverrides["title"] = d.share.Title
|
||||
shareOverrides["description"] = d.share.Description
|
||||
shareOverrides["themeColor"] = d.share.ThemeColor
|
||||
shareOverrides["quickDownload"] = d.share.QuickDownload
|
||||
shareOverrides["disableThumbnails"] = d.share.DisableThumbnails
|
||||
shareOverrides["disableFileViewer"] = d.share.DisablingFileViewer
|
||||
if d.share.Favicon != "" {
|
||||
if strings.HasPrefix(d.share.Favicon, "http") {
|
||||
data["favicon"] = d.share.Favicon
|
||||
} else {
|
||||
data["favicon"] = staticURL + "/" + d.share.Favicon
|
||||
shareProps["isShare"] = true
|
||||
shareProps["isValid"] = d.shareValid
|
||||
shareProps["hash"] = d.share.Hash
|
||||
|
||||
if d.shareValid {
|
||||
disableNavButtons = disableNavButtons || d.share.HideNavButtons
|
||||
shareProps["viewMode"] = d.share.ViewMode
|
||||
shareProps["banner"] = d.share.Banner
|
||||
shareProps["title"] = d.share.Title
|
||||
shareProps["description"] = d.share.Description
|
||||
shareProps["themeColor"] = d.share.ThemeColor
|
||||
shareProps["quickDownload"] = d.share.QuickDownload
|
||||
shareProps["disableThumbnails"] = d.share.DisableThumbnails
|
||||
shareProps["disableFileViewer"] = d.share.DisableFileViewer
|
||||
shareProps["disableShareCard"] = d.share.DisableShareCard
|
||||
shareProps["disableSidebar"] = d.share.DisableSidebar
|
||||
shareProps["isPasswordProtected"] = d.share.PasswordHash != ""
|
||||
shareProps["downloadURL"] = getDownloadURL(r, d.share.Hash)
|
||||
if d.share.Favicon != "" {
|
||||
if strings.HasPrefix(d.share.Favicon, "http") {
|
||||
data["favicon"] = d.share.Favicon
|
||||
} else {
|
||||
data["favicon"] = publicStaticURL + "/" + d.share.Favicon
|
||||
}
|
||||
}
|
||||
if d.share.Description != "" {
|
||||
data["description"] = d.share.Description
|
||||
}
|
||||
if d.share.Title != "" {
|
||||
data["title"] = d.share.Title
|
||||
}
|
||||
}
|
||||
if d.share.Description != "" {
|
||||
data["description"] = d.share.Description
|
||||
}
|
||||
if d.share.Title != "" {
|
||||
data["title"] = d.share.Title
|
||||
}
|
||||
|
||||
// base url could be different for routes behind proxy
|
||||
data["staticURL"] = publicStaticURL
|
||||
data["favicon"] = publicStaticURL + "/img/icons/favicon-256x256.png"
|
||||
data["winIcon"] = publicStaticURL + "/img/icons/mstile-144x144.png"
|
||||
data["appIcon"] = publicStaticURL + "/img/icons/android-chrome-256x256.png"
|
||||
}
|
||||
// variables consumed by frontend as json
|
||||
data["globalVars"] = map[string]interface{}{
|
||||
@@ -118,7 +151,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"updateAvailable": utils.GetUpdateAvailableUrl(),
|
||||
"disableNavButtons": disableNavButtons,
|
||||
"userSelectableThemes": config.Frontend.Styling.CustomThemeOptions,
|
||||
"share": shareOverrides,
|
||||
"share": shareProps,
|
||||
}
|
||||
jsonVars, err := json.Marshal(data["globalVars"])
|
||||
if err != nil {
|
||||
@@ -137,10 +170,9 @@ func staticFilesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
const maxAge = 86400 // 1 day
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%v", maxAge))
|
||||
w.Header().Set("Content-Security-Policy", `default-src 'self'; style-src 'unsafe-inline';`)
|
||||
// Remove "/static/" from the request path
|
||||
adjustedPath := strings.TrimPrefix(r.URL.Path, fmt.Sprintf("%vstatic/", config.Server.BaseURL))
|
||||
adjustedCompressed := adjustedPath + ".gz"
|
||||
if strings.HasSuffix(adjustedPath, ".js") {
|
||||
|
||||
adjustedCompressed := r.URL.Path + ".gz"
|
||||
if strings.HasSuffix(r.URL.Path, ".js") {
|
||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8") // Set the correct MIME type for JavaScript files
|
||||
}
|
||||
// Check if the gzipped version of the file exists
|
||||
@@ -153,7 +185,7 @@ func staticFilesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
} else {
|
||||
// Otherwise, serve the regular file
|
||||
http.StripPrefix(fmt.Sprintf("%vstatic/", config.Server.BaseURL), http.FileServer(http.FS(assetFs))).ServeHTTP(w, r)
|
||||
http.FileServer(http.FS(assetFs)).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+265
-60
@@ -56,7 +56,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Varies based on query. Can be access.FrontendAccessRule, []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule",
|
||||
"description": "Varies based on query. Can be access.FrontendAccessRule (when source and path specified), []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1665,6 +1665,56 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Creates a new share link with an optional expiration time and password protection.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Shares"
|
||||
],
|
||||
"summary": "Create a share link",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Share creation parameters",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/share.CreateBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Created share link",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/share.Link"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - failed to decode body",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/shares": {
|
||||
@@ -1701,61 +1751,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Creates a new share link with an optional expiration time and password protection.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Shares"
|
||||
],
|
||||
"summary": "Create a share link",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source Path of the files to share",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name of the files to share",
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Created share link",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/share.Link"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - failed to decode body",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Deletes a share link specified by its hash.",
|
||||
"consumes": [
|
||||
@@ -2135,10 +2130,9 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)",
|
||||
"description": "if specified, only the files in the list will be downloaded. eg. files=/file1||/folder/file2",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
@@ -2198,6 +2192,90 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/public/share/direct": {
|
||||
"get": {
|
||||
"description": "Creates a direct download link for a specific file with configurable duration, download count, and speed limits. If a share already exists with matching parameters, the existing share will be reused.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Shares"
|
||||
],
|
||||
"summary": "Create direct download link",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "File path to create download link for",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the file",
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Duration in minutes for link validity (default: 60)",
|
||||
"name": "duration",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Maximum number of downloads allowed (default: unlimited)",
|
||||
"name": "count",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Download speed limit in kbps (default: unlimited)",
|
||||
"name": "speed",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Direct download link created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.DirectDownloadResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - invalid parameters or path is not a file",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - access denied",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@@ -2221,6 +2299,20 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.DirectDownloadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.GroupListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2893,10 +2985,18 @@ const docTemplate = `{
|
||||
"description": "default \"/\" should match folders under path",
|
||||
"type": "string"
|
||||
},
|
||||
"denyByDefault": {
|
||||
"description": "deny access unless an \"allow\" access rule was specifically created.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableIndexing": {
|
||||
"description": "disable the indexing of this source",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disabled": {
|
||||
"description": "disable the source, this is useful so you don't need to remove it from the config file",
|
||||
"type": "boolean"
|
||||
},
|
||||
"exclude": {
|
||||
"description": "exclude files and folders from indexing, if include is not set",
|
||||
"allOf": [
|
||||
@@ -2914,7 +3014,7 @@ const docTemplate = `{
|
||||
]
|
||||
},
|
||||
"indexingIntervalMinutes": {
|
||||
"description": "optional manual overide interval in seconds to re-index the source",
|
||||
"description": "optional manual overide interval in minutes to re-index the source",
|
||||
"type": "integer"
|
||||
},
|
||||
"maxWatchers": {
|
||||
@@ -2927,6 +3027,10 @@ const docTemplate = `{
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"private": {
|
||||
"description": "designate as source as private -- currently just means no sharing permitted.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3073,6 +3177,94 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"share.CreateBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowUpload": {
|
||||
"description": "AllowEdit bool ` + "`" + `json:\"allowEdit,omitempty\"` + "`" + `",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowedUsernames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"banner": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableAnonymous": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableFileViewer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableShareCard": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableSidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableThumbnails": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"downloadURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"downloadsLimit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"expires": {
|
||||
"type": "string"
|
||||
},
|
||||
"favicon": {
|
||||
"type": "string"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideNavButtons": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"keepAfterExpiration": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"maxBandwidth": {
|
||||
"type": "integer"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"shareTheme": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"share.Link": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3098,9 +3290,21 @@ const docTemplate = `{
|
||||
"disableFileViewer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableShareCard": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableSidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableThumbnails": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"downloadURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"downloads": {
|
||||
"type": "integer"
|
||||
},
|
||||
"downloadsLimit": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -3135,6 +3339,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Varies based on query. Can be access.FrontendAccessRule, []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule",
|
||||
"description": "Varies based on query. Can be access.FrontendAccessRule (when source and path specified), []access.PrincipalRule, map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1654,6 +1654,56 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Creates a new share link with an optional expiration time and password protection.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Shares"
|
||||
],
|
||||
"summary": "Create a share link",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Share creation parameters",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/share.CreateBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Created share link",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/share.Link"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - failed to decode body",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/shares": {
|
||||
@@ -1690,61 +1740,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Creates a new share link with an optional expiration time and password protection.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Shares"
|
||||
],
|
||||
"summary": "Create a share link",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source Path of the files to share",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name of the files to share",
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Created share link",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/share.Link"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - failed to decode body",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Deletes a share link specified by its hash.",
|
||||
"consumes": [
|
||||
@@ -2124,10 +2119,9 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "a list of files in the following format 'filename' and separated by '||' with additional items in the list. (required)",
|
||||
"description": "if specified, only the files in the list will be downloaded. eg. files=/file1||/folder/file2",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
@@ -2187,6 +2181,90 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/public/share/direct": {
|
||||
"get": {
|
||||
"description": "Creates a direct download link for a specific file with configurable duration, download count, and speed limits. If a share already exists with matching parameters, the existing share will be reused.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Shares"
|
||||
],
|
||||
"summary": "Create direct download link",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "File path to create download link for",
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for the file",
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Duration in minutes for link validity (default: 60)",
|
||||
"name": "duration",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Maximum number of downloads allowed (default: unlimited)",
|
||||
"name": "count",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Download speed limit in kbps (default: unlimited)",
|
||||
"name": "speed",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Direct download link created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.DirectDownloadResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - invalid parameters or path is not a file",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - access denied",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@@ -2210,6 +2288,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.DirectDownloadResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.GroupListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2882,10 +2974,18 @@
|
||||
"description": "default \"/\" should match folders under path",
|
||||
"type": "string"
|
||||
},
|
||||
"denyByDefault": {
|
||||
"description": "deny access unless an \"allow\" access rule was specifically created.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableIndexing": {
|
||||
"description": "disable the indexing of this source",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disabled": {
|
||||
"description": "disable the source, this is useful so you don't need to remove it from the config file",
|
||||
"type": "boolean"
|
||||
},
|
||||
"exclude": {
|
||||
"description": "exclude files and folders from indexing, if include is not set",
|
||||
"allOf": [
|
||||
@@ -2903,7 +3003,7 @@
|
||||
]
|
||||
},
|
||||
"indexingIntervalMinutes": {
|
||||
"description": "optional manual overide interval in seconds to re-index the source",
|
||||
"description": "optional manual overide interval in minutes to re-index the source",
|
||||
"type": "integer"
|
||||
},
|
||||
"maxWatchers": {
|
||||
@@ -2916,6 +3016,10 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"private": {
|
||||
"description": "designate as source as private -- currently just means no sharing permitted.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3062,6 +3166,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"share.CreateBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowUpload": {
|
||||
"description": "AllowEdit bool `json:\"allowEdit,omitempty\"`",
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowedUsernames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"banner": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableAnonymous": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableFileViewer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableShareCard": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableSidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableThumbnails": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"downloadURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"downloadsLimit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"expires": {
|
||||
"type": "string"
|
||||
},
|
||||
"favicon": {
|
||||
"type": "string"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideNavButtons": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"keepAfterExpiration": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"maxBandwidth": {
|
||||
"type": "integer"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"shareTheme": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"share.Link": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3087,9 +3279,21 @@
|
||||
"disableFileViewer": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableShareCard": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableSidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableThumbnails": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"downloadURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"downloads": {
|
||||
"type": "integer"
|
||||
},
|
||||
"downloadsLimit": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -3124,6 +3328,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"description": "backend source is path to maintain between name changes",
|
||||
"type": "string"
|
||||
},
|
||||
"themeColor": {
|
||||
|
||||
@@ -12,6 +12,15 @@ definitions:
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
http.DirectDownloadResponse:
|
||||
properties:
|
||||
hash:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
http.GroupListResponse:
|
||||
properties:
|
||||
groups:
|
||||
@@ -506,9 +515,16 @@ definitions:
|
||||
defaultUserScope:
|
||||
description: default "/" should match folders under path
|
||||
type: string
|
||||
denyByDefault:
|
||||
description: deny access unless an "allow" access rule was specifically created.
|
||||
type: boolean
|
||||
disableIndexing:
|
||||
description: disable the indexing of this source
|
||||
type: boolean
|
||||
disabled:
|
||||
description: disable the source, this is useful so you don't need to remove
|
||||
it from the config file
|
||||
type: boolean
|
||||
exclude:
|
||||
allOf:
|
||||
- $ref: '#/definitions/settings.ExcludeIndexFilter'
|
||||
@@ -518,7 +534,7 @@ definitions:
|
||||
- $ref: '#/definitions/settings.IncludeIndexFilter'
|
||||
description: include files and folders from indexing, if exclude is not set
|
||||
indexingIntervalMinutes:
|
||||
description: optional manual overide interval in seconds to re-index the source
|
||||
description: optional manual overide interval in minutes to re-index the source
|
||||
type: integer
|
||||
maxWatchers:
|
||||
description: number of concurrent watchers to use for this source, currently
|
||||
@@ -530,6 +546,10 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
private:
|
||||
description: designate as source as private -- currently just means no sharing
|
||||
permitted.
|
||||
type: boolean
|
||||
type: object
|
||||
settings.StylingConfig:
|
||||
properties:
|
||||
@@ -643,6 +663,65 @@ definitions:
|
||||
description: 'view mode to use: eg. normal, list, grid, or compact'
|
||||
type: string
|
||||
type: object
|
||||
share.CreateBody:
|
||||
properties:
|
||||
allowUpload:
|
||||
description: AllowEdit bool `json:"allowEdit,omitempty"`
|
||||
type: boolean
|
||||
allowedUsernames:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
banner:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
disableAnonymous:
|
||||
type: boolean
|
||||
disableFileViewer:
|
||||
type: boolean
|
||||
disableShareCard:
|
||||
type: boolean
|
||||
disableSidebar:
|
||||
type: boolean
|
||||
disableThumbnails:
|
||||
type: boolean
|
||||
downloadURL:
|
||||
type: string
|
||||
downloadsLimit:
|
||||
type: integer
|
||||
expires:
|
||||
type: string
|
||||
favicon:
|
||||
type: string
|
||||
hash:
|
||||
type: string
|
||||
hideNavButtons:
|
||||
type: boolean
|
||||
keepAfterExpiration:
|
||||
type: boolean
|
||||
maxBandwidth:
|
||||
type: integer
|
||||
password:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
quickDownload:
|
||||
type: boolean
|
||||
shareTheme:
|
||||
type: string
|
||||
source:
|
||||
description: backend source is path to maintain between name changes
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
viewMode:
|
||||
type: string
|
||||
type: object
|
||||
share.Link:
|
||||
properties:
|
||||
allowUpload:
|
||||
@@ -660,8 +739,16 @@ definitions:
|
||||
type: boolean
|
||||
disableFileViewer:
|
||||
type: boolean
|
||||
disableShareCard:
|
||||
type: boolean
|
||||
disableSidebar:
|
||||
type: boolean
|
||||
disableThumbnails:
|
||||
type: boolean
|
||||
downloadURL:
|
||||
type: string
|
||||
downloads:
|
||||
type: integer
|
||||
downloadsLimit:
|
||||
type: integer
|
||||
expire:
|
||||
@@ -685,6 +772,7 @@ definitions:
|
||||
shareTheme:
|
||||
type: string
|
||||
source:
|
||||
description: backend source is path to maintain between name changes
|
||||
type: string
|
||||
themeColor:
|
||||
type: string
|
||||
@@ -994,8 +1082,9 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Varies based on query. Can be access.FrontendAccessRule, []access.PrincipalRule,
|
||||
map[string][]access.PrincipalRule, or map[string]access.FrontendAccessRule
|
||||
description: Varies based on query. Can be access.FrontendAccessRule (when
|
||||
source and path specified), []access.PrincipalRule, map[string][]access.PrincipalRule,
|
||||
or map[string]access.FrontendAccessRule
|
||||
schema:
|
||||
type: object
|
||||
"400":
|
||||
@@ -2023,6 +2112,40 @@ paths:
|
||||
summary: Get share links by path
|
||||
tags:
|
||||
- Shares
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates a new share link with an optional expiration time and password
|
||||
protection.
|
||||
parameters:
|
||||
- description: Share creation parameters
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/share.CreateBody'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Created share link
|
||||
schema:
|
||||
$ref: '#/definitions/share.Link'
|
||||
"400":
|
||||
description: Bad request - failed to decode body
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Create a share link
|
||||
tags:
|
||||
- Shares
|
||||
/api/shares:
|
||||
delete:
|
||||
consumes:
|
||||
@@ -2077,44 +2200,6 @@ paths:
|
||||
summary: List share links
|
||||
tags:
|
||||
- Shares
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates a new share link with an optional expiration time and password
|
||||
protection.
|
||||
parameters:
|
||||
- description: Source Path of the files to share
|
||||
in: query
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name of the files to share
|
||||
in: query
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Created share link
|
||||
schema:
|
||||
$ref: '#/definitions/share.Link'
|
||||
"400":
|
||||
description: Bad request - failed to decode body
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Create a share link
|
||||
tags:
|
||||
- Shares
|
||||
/api/signup:
|
||||
post:
|
||||
consumes:
|
||||
@@ -2332,11 +2417,10 @@ paths:
|
||||
description: Returns the raw content of a file, multiple files, or a directory.
|
||||
Supports downloading files as archives in various formats.
|
||||
parameters:
|
||||
- description: a list of files in the following format 'filename' and separated
|
||||
by '||' with additional items in the list. (required)
|
||||
- description: if specified, only the files in the list will be downloaded.
|
||||
eg. files=/file1||/folder/file2
|
||||
in: query
|
||||
name: files
|
||||
required: true
|
||||
type: string
|
||||
- description: If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults
|
||||
to 'attachment'.
|
||||
@@ -2382,4 +2466,62 @@ paths:
|
||||
summary: Get raw content of a file, multiple files, or directory
|
||||
tags:
|
||||
- Resources
|
||||
/public/share/direct:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates a direct download link for a specific file with configurable
|
||||
duration, download count, and speed limits. If a share already exists with
|
||||
matching parameters, the existing share will be reused.
|
||||
parameters:
|
||||
- description: File path to create download link for
|
||||
in: query
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name for the file
|
||||
in: query
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
- description: 'Duration in minutes for link validity (default: 60)'
|
||||
in: query
|
||||
name: duration
|
||||
type: string
|
||||
- description: 'Maximum number of downloads allowed (default: unlimited)'
|
||||
in: query
|
||||
name: count
|
||||
type: string
|
||||
- description: 'Download speed limit in kbps (default: unlimited)'
|
||||
in: query
|
||||
name: speed
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Direct download link created
|
||||
schema:
|
||||
$ref: '#/definitions/http.DirectDownloadResponse'
|
||||
"400":
|
||||
description: Bad request - invalid parameters or path is not a file
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"403":
|
||||
description: Forbidden - access denied
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Create direct download link
|
||||
tags:
|
||||
- Shares
|
||||
swagger: "2.0"
|
||||
@@ -1 +0,0 @@
|
||||
frontend
|
||||
@@ -8,11 +8,10 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && cp -r dist/* ../backend/http/embed",
|
||||
"build-windows": "vite build && robocopy dist ../backend/http/embed /e",
|
||||
"build": "vite build --outDir ../backend/http/dist && cp -r ../backend/http/dist/* ../backend/http/embed",
|
||||
"build-windows": "vite build --outDir ../backend/http/dist && robocopy ../backend/http/dist ../backend/http/embed /e",
|
||||
"build-docker": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"watch": "vite build --watch --outDir ../backend/http/dist",
|
||||
"typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
|
||||
"lint": "eslint --ext .js,.vue,ts src",
|
||||
"lint:fix": "eslint --fix src/",
|
||||
|
||||
@@ -21,7 +21,10 @@ server:
|
||||
- path: "/relative/or/absolute/path" # file system path. (Can be relative) validate:required
|
||||
name: "backend" # display name
|
||||
config:
|
||||
indexingIntervalMinutes: 0 # optional manual overide interval in seconds to re-index the source
|
||||
denyByDefault: false # deny access unless an "allow" access rule was specifically created.
|
||||
private: false # designate as source as private -- currently just means no sharing permitted.
|
||||
disabled: false # disable the source, this is useful so you don't need to remove it from the config file
|
||||
indexingIntervalMinutes: 0 # optional manual overide interval in minutes to re-index the source
|
||||
disableIndexing: false # disable the indexing of this source
|
||||
maxWatchers: 0 # number of concurrent watchers to use for this source, currently not supported
|
||||
neverWatchPaths: []
|
||||
@@ -97,58 +100,7 @@ frontend:
|
||||
customThemes: # A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option.
|
||||
alternative:
|
||||
description: Reduce rounded corners
|
||||
css: |-
|
||||
.card,
|
||||
#listingView.gallery .item,
|
||||
#listingView.normal .item,
|
||||
.button,
|
||||
button,
|
||||
.button-action,
|
||||
.input,
|
||||
input,
|
||||
#listingView.gallery .item img,
|
||||
#listingView .item img,
|
||||
.sizeInputWrapper,
|
||||
.sizeInput,
|
||||
#search-input,
|
||||
.floating-tooltip,
|
||||
.settings-items>.item,
|
||||
.button-group {
|
||||
border-radius: 0.25em !important;
|
||||
}
|
||||
|
||||
#listingView.compact .header {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
#listingView.list .item:last-child,
|
||||
#results {
|
||||
border-bottom-left-radius: 0.25em !important;
|
||||
border-bottom-right-radius: 0.25em !important;
|
||||
}
|
||||
|
||||
#listingView.list .item:first-child {
|
||||
border-top-left-radius: 0.25em !important;
|
||||
border-top-right-radius: 0.25em !important;
|
||||
}
|
||||
|
||||
#breadcrumbs ul li:last-child a {
|
||||
border-top-right-radius: 0.25em !important;
|
||||
border-bottom-right-radius: 0.25em !important;
|
||||
}
|
||||
#breadcrumbs ul li:first-child a {
|
||||
border-top-left-radius: 0.25em !important;
|
||||
border-bottom-left-radius: 0.25em !important;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
border-radius: 0.1em !important;
|
||||
}
|
||||
css: reduce-rounded-corners.css
|
||||
default:
|
||||
description: The default theme
|
||||
css: ""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fetchURL, fetchJSON, adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
import { getApiPath, getPublicApiPath } from "@/utils/url.js";
|
||||
import { externalUrl } from "@/utils/constants";
|
||||
import { state } from "@/store";
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function fetchPub(path, hash, password = "", content = false) {
|
||||
...(content && { content: 'true' }),
|
||||
...(state.share.token && { token: state.share.token })
|
||||
}
|
||||
const apiPath = getApiPath("public/api/shared", params);
|
||||
const apiPath = getPublicApiPath("resources", params);
|
||||
const response = await fetch(apiPath, {
|
||||
headers: {
|
||||
"X-SHARE-PASSWORD": password ? encodeURIComponent(password) : "",
|
||||
@@ -63,7 +63,7 @@ export function getDownloadURL(share, files, inline=false) {
|
||||
token: share.token,
|
||||
...(inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getApiPath("public/api/raw", params);
|
||||
const apiPath = getPublicApiPath("raw", params);
|
||||
return window.origin + apiPath
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export function getPreviewURL(path,size="small") {
|
||||
inline: 'true',
|
||||
...(state.share.token && { token: state.share.token })
|
||||
}
|
||||
const apiPath = getApiPath('public/api/preview', params)
|
||||
const apiPath = getPublicApiPath('preview', params)
|
||||
return window.origin + apiPath
|
||||
} catch (/** @type {any} */ err) {
|
||||
notify.showError(err.message || 'Error getting preview URL')
|
||||
@@ -132,14 +132,11 @@ export async function remove(hash) {
|
||||
|
||||
// Create a new share
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {string} source
|
||||
* @param {Record<string, any>} bodyObj
|
||||
* @returns {Promise<Share>}
|
||||
*/
|
||||
export async function create(path, source, bodyObj = {}) {
|
||||
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
|
||||
const apiPath = getApiPath("public/share", params);
|
||||
export async function create(bodyObj = {}) {
|
||||
const apiPath = getApiPath("public/share");
|
||||
return fetchJSON(apiPath, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(bodyObj || {}),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { fetchURL, fetchJSON } from '@/api/utils'
|
||||
import { getApiPath } from '@/utils/url.js'
|
||||
import { getApiPath, getPublicApiPath } from '@/utils/url.js'
|
||||
import { notify } from '@/notify' // Import notify for error handling
|
||||
import { setNewToken } from '@/utils/auth.js' // Import setNewToken for token management
|
||||
import { shareInfo } from '@/utils/constants'
|
||||
|
||||
export async function getAllUsers () {
|
||||
export async function getAllUsers() {
|
||||
try {
|
||||
const apiPath = getApiPath('api/users')
|
||||
return await fetchJSON(apiPath)
|
||||
@@ -13,7 +14,7 @@ export async function getAllUsers () {
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateOTP (username, password) {
|
||||
export async function generateOTP(username, password) {
|
||||
const params = { username }
|
||||
try {
|
||||
let apiPath = getApiPath('api/auth/otp/generate', params)
|
||||
@@ -30,7 +31,7 @@ export async function generateOTP (username, password) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyOtp (username, password, otp) {
|
||||
export async function verifyOtp(username, password, otp) {
|
||||
const params = { username }
|
||||
try {
|
||||
let apiPath = getApiPath('api/auth/otp/verify', params)
|
||||
@@ -91,7 +92,10 @@ export async function login(username, password, recaptcha, otp) {
|
||||
}
|
||||
export async function get(id) {
|
||||
try {
|
||||
const apiPath = getApiPath('api/users', { id: id })
|
||||
let apiPath = getApiPath('api/users', { id: id })
|
||||
if (shareInfo.isShare) {
|
||||
apiPath = getPublicApiPath('users', { id: id })
|
||||
}
|
||||
return await fetchJSON(apiPath)
|
||||
} catch (err) {
|
||||
notify.showError(err.message || `Failed to fetch user with ID: ${id}`)
|
||||
@@ -148,7 +152,7 @@ export function deleteApiKey (params) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function create (user) {
|
||||
export async function create(user) {
|
||||
try {
|
||||
const apiPath = getApiPath('api/users')
|
||||
const res = await fetchURL(apiPath, {
|
||||
@@ -190,7 +194,7 @@ export async function update(user, which = ['all']) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function remove (id) {
|
||||
export async function remove(id) {
|
||||
try {
|
||||
const apiPath = getApiPath('api/users', { id: id })
|
||||
await fetchURL(apiPath, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { state } from "@/store";
|
||||
import { renew, logout } from "@/utils/auth";
|
||||
import { notify } from "@/notify";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export async function fetchURL(url, opts, auth = true) {
|
||||
opts = opts || {};
|
||||
@@ -34,7 +35,7 @@ export async function fetchURL(url, opts, auth = true) {
|
||||
let error = new Error(await res.text());
|
||||
error.status = res.status;
|
||||
|
||||
if (auth && res.status == 401) {
|
||||
if (auth && res.status == 401 && !shareInfo.isShare) {
|
||||
logout();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
return getters.multibuttonState();
|
||||
},
|
||||
stickSidebar() {
|
||||
return state.stickSidebar;
|
||||
return state.user?.stickySidebar;
|
||||
},
|
||||
currentView() {
|
||||
return getters.currentView();
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="isCardView" class="gallery-size card">
|
||||
<div v-if="showGallerySize" class="gallery-size card">
|
||||
{{ $t("files.size") }}<span class="sr-only">:</span> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<input
|
||||
v-model="gallerySize"
|
||||
@@ -69,8 +69,8 @@ export default {
|
||||
addPadding() {
|
||||
return getters.isStickySidebar() || getters.isShare();
|
||||
},
|
||||
isCardView() {
|
||||
return getters.isCardView();
|
||||
showGallerySize() {
|
||||
return getters.isCardView() && state.req?.items?.length > 0;
|
||||
},
|
||||
items() {
|
||||
const req = state.req;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<button
|
||||
v-for="(btn, index) in buttons"
|
||||
:key="index"
|
||||
class="clickable"
|
||||
:class="{ active: activeButton === index }"
|
||||
@click="setActiveButton(index, btn.value)"
|
||||
>
|
||||
|
||||
@@ -17,47 +17,37 @@
|
||||
<div v-if="selectedCount > 0" class="button selected-count-header">
|
||||
<span>{{ selectedCount }} {{ $t("prompts.selected") }} </span>
|
||||
</div>
|
||||
|
||||
<action
|
||||
v-if="!showCreate && !isSearchActive && userPerms.modify"
|
||||
v-if="!showCreate && !isSearchActive && userPerms.modify && !isShare"
|
||||
icon="add"
|
||||
:label="$t('buttons.new')"
|
||||
@action="startShowCreate"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="showCreate && !isSearchActive && userPerms.modify"
|
||||
icon="create_new_folder"
|
||||
:label="$t('sidebar.newFolder')"
|
||||
@action="showHover('newDir')"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="showCreate && userPerms.modify && !isSearchActive"
|
||||
icon="note_add"
|
||||
:label="$t('sidebar.newFile')"
|
||||
@action="showHover('newFile')"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="showCreate && userPerms.modify && !isSearchActive"
|
||||
icon="file_upload"
|
||||
:label="$t('buttons.upload')"
|
||||
@action="uploadFunc"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="!showCreate && selectedCount == 1"
|
||||
icon="info"
|
||||
:label="$t('buttons.info')"
|
||||
show="info"
|
||||
/>
|
||||
<action
|
||||
v-if="showSelectMultiple"
|
||||
icon="check_circle"
|
||||
:label="$t('buttons.selectMultiple')"
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="(!showCreate && selectedCount > 0)"
|
||||
icon="file_download"
|
||||
@@ -69,13 +59,13 @@
|
||||
v-if="selectedCount <= 1 && showShare"
|
||||
icon="share"
|
||||
:label="$t('buttons.share')"
|
||||
show="share"
|
||||
@action="showShareHover"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount == 1 && userPerms.modify && !isSearchActive"
|
||||
icon="mode_edit"
|
||||
:label="$t('buttons.rename')"
|
||||
show="rename"
|
||||
@action="showRenameHover"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
@@ -83,6 +73,12 @@
|
||||
:label="$t('buttons.copyFile')"
|
||||
show="copy"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount == 1 && isSearchActive"
|
||||
icon="folder"
|
||||
:label="$t('buttons.openParentFolder')"
|
||||
@action="openParentFolder"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
icon="forward"
|
||||
@@ -90,13 +86,7 @@
|
||||
show="move"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
icon="file_upload"
|
||||
:label="$t('buttons.upload')"
|
||||
@action="showUpload"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
v-if="showDelete"
|
||||
icon="delete"
|
||||
:label="$t('buttons.delete')"
|
||||
show="delete"
|
||||
@@ -107,6 +97,12 @@
|
||||
:label="$t('access.rules')"
|
||||
@action="showAccessHover"
|
||||
/>
|
||||
<action
|
||||
v-if="showSelectMultiple"
|
||||
icon="check_circle"
|
||||
:label="$t('buttons.selectMultiple')"
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
<transition
|
||||
@@ -127,6 +123,7 @@
|
||||
:class="{ 'dark-mode': isDarkMode }"
|
||||
>
|
||||
<action v-if="showGoToRaw" icon="open_in_new" :label="$t('buttons.openFile')" @action="goToRaw()" />
|
||||
<action v-if="shouldShowParentFolder()" icon="folder" :label="$t('buttons.openParentFolder')" @action="openParentFolder" />
|
||||
<action v-if="isPreview" icon="file_download" :label="$t('buttons.download')" @action="startDownload" />
|
||||
<action v-if="showEdit" icon="edit" :label="$t('buttons.edit')" @action="edit()" />
|
||||
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
|
||||
@@ -144,6 +141,7 @@ import buttons from "@/utils/buttons";
|
||||
import { notify } from "@/notify";
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
import { filesApi, publicApi } from "@/api";
|
||||
import { url } from "@/utils";
|
||||
export default {
|
||||
name: "ContextMenu",
|
||||
components: {
|
||||
@@ -165,6 +163,9 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isShare() {
|
||||
return getters.isShare();
|
||||
},
|
||||
showSelectMultiple() {
|
||||
if (this.isMultiple || this.isSearchActive) {
|
||||
return false;
|
||||
@@ -194,11 +195,12 @@ export default {
|
||||
},
|
||||
showDelete() {
|
||||
const cv = getters.currentView();
|
||||
if (getters.isShare()) {
|
||||
if (getters.isShare() || !state.user?.permissions?.modify) {
|
||||
// TODO: add support for deleting shared files
|
||||
return false;
|
||||
}
|
||||
return state.user?.permissions?.modify && (cv == "preview" || cv == "onlyOfficeEditor" || cv == "markdownViewer" || cv == "epubViewer" || cv == "docViewer" || cv == "editor");
|
||||
const showDelete = cv != "settings" && !this.showCreate && this.selectedCount > 0 && !this.isSearchActive;
|
||||
return showDelete;
|
||||
},
|
||||
isPreview() {
|
||||
const cv = getters.currentView();
|
||||
@@ -225,13 +227,10 @@ export default {
|
||||
return state.user?.permissions?.admin && this.showCreate;
|
||||
},
|
||||
showShare() {
|
||||
return (
|
||||
state.user?.permissions &&
|
||||
state.user?.permissions.share &&
|
||||
state.user.username != "anonymous" &&
|
||||
!getters.isShare() &&
|
||||
!this.isSearchActive
|
||||
);
|
||||
if (getters.isShare()) {
|
||||
return false;
|
||||
}
|
||||
return state.user?.permissions?.share;
|
||||
},
|
||||
showContext() {
|
||||
return getters.currentPromptName() == "ContextMenu";
|
||||
@@ -267,8 +266,19 @@ export default {
|
||||
modify: state.user?.permissions?.modify,
|
||||
};
|
||||
},
|
||||
currentPrompt() {
|
||||
return getters.currentPrompt();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentPrompt: {
|
||||
handler(prompt) {
|
||||
if (prompt && prompt.name === "ContextMenu") {
|
||||
this.setPositions();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
hasOverflowItems: {
|
||||
handler(hasItems) {
|
||||
mutations.setContextMenuHasItems(hasItems);
|
||||
@@ -296,6 +306,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shouldShowParentFolder() {
|
||||
return this.isPreview && state.req.path != "/";
|
||||
},
|
||||
showAccessHover() {
|
||||
mutations.showHover({
|
||||
name: "access",
|
||||
@@ -368,6 +381,9 @@ export default {
|
||||
}, 300);
|
||||
},
|
||||
startShowCreate() {
|
||||
if (getters.isShare()) {
|
||||
return;
|
||||
}
|
||||
this.showCreate = true;
|
||||
},
|
||||
uploadFunc() {
|
||||
@@ -376,6 +392,22 @@ export default {
|
||||
showHover(value) {
|
||||
return mutations.showHover(value);
|
||||
},
|
||||
showShareHover() {
|
||||
mutations.showHover({
|
||||
name: "share",
|
||||
props: {
|
||||
item: getters.selectedCount() == 1 ? getters.getFirstSelected() : state.req
|
||||
},
|
||||
});
|
||||
},
|
||||
showRenameHover() {
|
||||
mutations.showHover({
|
||||
name: "rename",
|
||||
props: {
|
||||
item: getters.selectedCount() == 1 ? getters.getFirstSelected() : state.req
|
||||
},
|
||||
});
|
||||
},
|
||||
setPositions() {
|
||||
const contextProps = getters.currentPrompt().props;
|
||||
this.posX = contextProps.posX;
|
||||
@@ -383,7 +415,7 @@ export default {
|
||||
},
|
||||
initializeCreateState() {
|
||||
// Only set initial showCreate state, don't override user choices
|
||||
if (state.selected.length > 0) {
|
||||
if (state.selected.length > 0 || getters.isShare()) {
|
||||
this.showCreate = false;
|
||||
} else {
|
||||
this.showCreate = true;
|
||||
@@ -395,7 +427,8 @@ export default {
|
||||
},
|
||||
startDownload() {
|
||||
mutations.closeHovers();
|
||||
downloadFiles(state.selected);
|
||||
const items = state.selected.length > 0 ? state.selected : [state.req];
|
||||
downloadFiles(items);
|
||||
},
|
||||
goToRaw() {
|
||||
if (getters.isShare()) {
|
||||
@@ -437,6 +470,14 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
openParentFolder() {
|
||||
const item = state.selected.length > 0 ? state.selected[0] : state.req;
|
||||
let parentPath = url.removeLastDir(item.path);
|
||||
if (parentPath == "") {
|
||||
parentPath = "/";
|
||||
}
|
||||
url.goToItem(state.req.source, parentPath);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="inputWrapper" style="display: flex">
|
||||
<select
|
||||
v-if="multipleSources"
|
||||
class="searchContext input"
|
||||
class="searchContext button input"
|
||||
aria-label="search-path"
|
||||
v-model="selectedSource"
|
||||
:value="selectedSource"
|
||||
@@ -122,7 +122,7 @@
|
||||
<li
|
||||
v-for="(s, k) in results"
|
||||
:key="k"
|
||||
class="search-entry"
|
||||
class="search-entry clickable"
|
||||
:class="{ active: activeStates[k] }"
|
||||
:aria-label="baseName(s.path)"
|
||||
>
|
||||
@@ -306,13 +306,15 @@ export default {
|
||||
return this.showHelp;
|
||||
},
|
||||
activeStates() {
|
||||
// Create a Set of combined `name` and `type` keys for efficient lookup
|
||||
const selectedSet = new Set(
|
||||
state.selected.map((item) => `${item.name}:${item.type}`)
|
||||
);
|
||||
const result = this.results.map((s) => selectedSet.has(`${s.name}:${s.type}`));
|
||||
// Build a map of active states for the `results` array
|
||||
return result;
|
||||
const selectedItems = state.selected ? [].concat(state.selected) : [];
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
// Return an array of all false if nothing is selected
|
||||
return new Array(this.results.length).fill(false);
|
||||
}
|
||||
|
||||
const selectedPaths = new Set(selectedItems.map((item) => item.path));
|
||||
return this.results.map((result) => selectedPaths.has(result.path));
|
||||
},
|
||||
sourceInfo() {
|
||||
return state.sources.info;
|
||||
@@ -519,17 +521,13 @@ export default {
|
||||
},
|
||||
addSelected(event, s) {
|
||||
const pathParts = url.removeTrailingSlash(s.path).split("/");
|
||||
let path = this.getContext + s.path;
|
||||
if (this.getContext === "/") {
|
||||
path = s.path;
|
||||
}
|
||||
let path = this.getContext + url.removeTrailingSlash(s.path);
|
||||
const modifiedItem = {
|
||||
name: pathParts.pop(),
|
||||
path: path,
|
||||
size: s.size,
|
||||
type: s.type,
|
||||
source: this.selectedSource || state.sources.current,
|
||||
fullPath: path,
|
||||
};
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(modifiedItem);
|
||||
@@ -538,7 +536,10 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.sizeInputWrapper {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.main-input {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -551,7 +552,6 @@ export default {
|
||||
border-left: 1px solid gray;
|
||||
border-right: 1px solid gray;
|
||||
word-wrap: break-word;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.searchContext.input {
|
||||
@@ -562,7 +562,16 @@ export default {
|
||||
width: 25%;
|
||||
min-width: 7em;
|
||||
max-width: 15em;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.searchContext.input option {
|
||||
background: grey;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.searchContext.input option:hover {
|
||||
background: var(--primaryColor);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#results > #result-list {
|
||||
@@ -670,9 +679,8 @@ export default {
|
||||
/* IE and Edge */
|
||||
}
|
||||
|
||||
.search-entry {
|
||||
cursor: pointer;
|
||||
border-radius: 0.25em;
|
||||
.search-entry:hover {
|
||||
background-color: var(--alt-background);
|
||||
}
|
||||
|
||||
.search-entry.active {
|
||||
@@ -864,4 +872,72 @@ body.rtl #search .boxes h3 {
|
||||
padding-right: 0.5em;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
#search {
|
||||
min-width: unset;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
#search.active {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#search-input {
|
||||
transition: 1s ease all;
|
||||
}
|
||||
|
||||
#search.active #search-input {
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0.075);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(6px);
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
#search.active>div {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
#search.active #result {
|
||||
height: 100vh;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#search.active #result>p>i {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
display: table;
|
||||
}
|
||||
|
||||
#search.active #result ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .3em 0;
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
#search-input>.action,
|
||||
#search-input>i {
|
||||
margin-right: 0.3em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#result-list {
|
||||
width:100vw !important;
|
||||
max-width: 100vw !important;
|
||||
left: 0;
|
||||
top: 4em;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -19,7 +19,7 @@
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<i :class="[classes, { active: active }]" class="icon"> {{ materialIcon }} </i>
|
||||
<i :class="[classes, { active: active, clickable: clickable }]" class="icon"> {{ materialIcon }} </i>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from "@/utils/constants";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import { mutations, state } from "@/store";
|
||||
import { shareOverrides } from "@/utils/constants";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
// NEW: Define placeholder and error image URLs for easy configuration
|
||||
const PLACEHOLDER_URL = baseURL + "static/img/placeholder.png"; // A generic loading placeholder
|
||||
@@ -56,6 +56,10 @@ export default {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -72,7 +76,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
hasPreviewImage() {
|
||||
if (shareOverrides.disableThumbnails) {
|
||||
if (shareInfo.disableThumbnails) {
|
||||
return false;
|
||||
}
|
||||
return this.isPreviewImg && this.imageState !== 'error' && !this.disablePreviewExt && !this.officeFileDisabled
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<a
|
||||
:href="getUrl()"
|
||||
class="item listing-item clickable no-select"
|
||||
:class="{
|
||||
item: true,
|
||||
'no-select': true,
|
||||
'listing-item': true,
|
||||
activebutton: isSelected,
|
||||
hiddenFile: isHiddenNotSelected && this && !this.isDraggedOver,
|
||||
'half-selected': isDraggedOver,
|
||||
@@ -62,6 +60,7 @@
|
||||
role="button"
|
||||
aria-label="Download"
|
||||
tabindex="0"
|
||||
:clickable=true
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
@@ -76,7 +75,7 @@ import * as upload from "@/utils/upload";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { url } from "@/utils";
|
||||
import Icon from "@/components/files/Icon.vue";
|
||||
import { baseURL, serverHasMultipleSources, shareOverrides } from "@/utils/constants";
|
||||
import { baseURL, serverHasMultipleSources, shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "item",
|
||||
@@ -116,7 +115,7 @@ export default {
|
||||
// @ts-ignore
|
||||
if (getters.isShare()) {
|
||||
// @ts-ignore
|
||||
return shareOverrides.quickDownload && !this.isDir;
|
||||
return shareInfo.quickDownload && !this.isDir;
|
||||
}
|
||||
// @ts-ignore
|
||||
return state.user?.quickDownload && !this.galleryView && !this.isDir;
|
||||
|
||||
+13
-13
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="share card">
|
||||
<div class="share__box">
|
||||
<div v-if="shareOverrides.banner" class="banner">
|
||||
<div v-if="shareInfo.banner" class="banner">
|
||||
<img :src="getShareBanner" />
|
||||
</div>
|
||||
<div v-if="shareOverrides.title" class="share__box__element">
|
||||
<h2>{{ shareOverrides.title }}</h2>
|
||||
<div v-if="shareInfo.title" class="share__box__element">
|
||||
<h3>{{ shareInfo.title }}</h3>
|
||||
</div>
|
||||
<div v-if="shareOverrides.description" class="share__box__element">
|
||||
<p>{{ shareOverrides.description }}</p>
|
||||
<div v-if="shareInfo.description" class="share__box__element">
|
||||
<p>{{ shareInfo.description }}</p>
|
||||
</div>
|
||||
<hr v-if="shareOverrides.banner || shareOverrides.title || shareOverrides.description" />
|
||||
<hr v-if="shareInfo.banner || shareInfo.title || shareInfo.description" />
|
||||
<div class="share__box__element">
|
||||
<strong>{{ $t("prompts.displayName") }}</strong> {{ req.name }}
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="share__box__element share__box__center">
|
||||
<button class="button button--flat" @click="goToLink()"> {{ $t("buttons.download") }} </button>
|
||||
<button class="button button--flat clickable" @click="goToLink()"> {{ $t("buttons.download") }} </button>
|
||||
</div>
|
||||
|
||||
<div v-if="req.type" class="share__box__element share__box__center">
|
||||
@@ -38,7 +38,7 @@ import { state, getters } from "@/store";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
import { shareOverrides } from "@/utils/constants";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "ShareInfo",
|
||||
@@ -61,13 +61,13 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
getShareBanner() {
|
||||
if (this.shareOverrides.banner.startsWith("http")) {
|
||||
return this.shareOverrides.banner;
|
||||
if (this.shareInfo.banner.startsWith("http")) {
|
||||
return this.shareInfo.banner;
|
||||
}
|
||||
return publicApi.getDownloadURL(state.share, [this.shareOverrides.banner]);
|
||||
return publicApi.getDownloadURL(state.share, [this.shareInfo.banner]);
|
||||
},
|
||||
shareOverrides() {
|
||||
return shareOverrides;
|
||||
shareInfo() {
|
||||
return shareInfo;
|
||||
},
|
||||
req() {
|
||||
return state.req;
|
||||
@@ -16,11 +16,24 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t("prompts.source", { suffix: ":" }) }} {{ currentSource }}</p>
|
||||
<div aria-label="access-path" class="searchContext" @click="startPathEdit" style="cursor: pointer;">
|
||||
<div aria-label="access-path" class="searchContext clickable button" @click="startPathEdit">
|
||||
{{ $t("search.path") }} {{ currentPath }}
|
||||
</div>
|
||||
<!-- Default behavior banner -->
|
||||
<div class="card item">
|
||||
<div class="card-content banner-content">
|
||||
<i class="material-icons">{{ sourceDenyDefault ? 'block' : 'check_circle' }}</i> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
{{ $t("access.defaultBehavior", { suffix: ":" }) }} {{ sourceDenyDefault ? $t("access.deny") : $t("access.allow")
|
||||
}}
|
||||
<i class="no-select material-symbols-outlined tooltip-info-icon"
|
||||
@mouseenter="showTooltip($event, $t('access.defaultBehaviorDescription'))" @mouseleave="hideTooltip">
|
||||
help
|
||||
</i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Add Form -->
|
||||
<div class="form-flex-group" >
|
||||
<div class="form-flex-group">
|
||||
<select class="input flat-right form-compact" v-model="addType">
|
||||
<option value="user">{{ $t("general.user") }}</option>
|
||||
<option value="group">{{ $t("general.group") }}</option>
|
||||
@@ -30,7 +43,8 @@
|
||||
<option value="deny">{{ $t("access.deny") }}</option>
|
||||
<option value="allow">{{ $t("access.allow") }}</option>
|
||||
</select>
|
||||
<input v-if="addType !== 'all'" class="input flat-right flat-left form-grow form-compact" v-model="addName" :placeholder="$t('access.enterName')" list="group-suggestions" />
|
||||
<input v-if="addType !== 'all'" class="input flat-right flat-left form-grow form-compact" v-model="addName"
|
||||
:placeholder="$t('access.enterName')" list="group-suggestions" />
|
||||
<datalist id="group-suggestions">
|
||||
<option v-for="group in groups" :key="group" :value="group"></option>
|
||||
</datalist>
|
||||
@@ -48,7 +62,8 @@
|
||||
</tr>
|
||||
<tr v-for="entry in entries" :key="entry.type + '-' + entry.name">
|
||||
<td>{{ entry.allow ? $t("access.allow") : $t("access.deny") }}</td>
|
||||
<td>{{ entry.type === 'user' ? $t("general.user") : (entry.type === 'group' ? $t("general.group") : $t('access.all')) }}</td>
|
||||
<td>{{ entry.type === 'user' ? $t("general.user") : (entry.type === 'group' ? $t("general.group") :
|
||||
$t('access.all')) }}</td>
|
||||
<td>{{ entry.name }}</td>
|
||||
<td>
|
||||
<button @click="deleteAccess(entry)" class="action" :aria-label="$t('buttons.delete')"
|
||||
@@ -62,11 +77,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button v-if="!isEditingPath" @click="closeHovers" class="button button--flat button--grey" :aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
</div>
|
||||
<button v-if="!isEditingPath" @click="closeHovers" class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -90,6 +105,7 @@ export default {
|
||||
currentSource: this.sourceName,
|
||||
tempSource: this.sourceName,
|
||||
rule: { denyAll: false, deny: { users: [], groups: [] }, allow: { users: [], groups: [] } },
|
||||
sourceDenyDefault: false,
|
||||
addType: "user",
|
||||
addListType: "deny",
|
||||
addName: "",
|
||||
@@ -171,10 +187,14 @@ export default {
|
||||
},
|
||||
async fetchRule() {
|
||||
try {
|
||||
this.rule = await accessApi.get(this.currentSource, this.currentPath);
|
||||
const response = await accessApi.get(this.currentSource, this.currentPath);
|
||||
// Handle new API response structure - now sourceDenyDefault is part of the rule
|
||||
this.rule = response;
|
||||
this.sourceDenyDefault = response.sourceDenyDefault || false;
|
||||
} catch (e) {
|
||||
notify.showError(e);
|
||||
this.rule = { denyAll: false, deny: { users: [], groups: [] }, allow: { users: [], groups: [] } };
|
||||
this.sourceDenyDefault = false;
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -225,6 +245,16 @@ export default {
|
||||
} else {
|
||||
this.$emit('close');
|
||||
}
|
||||
},
|
||||
showTooltip(event, text) {
|
||||
mutations.showTooltip({
|
||||
content: text,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
},
|
||||
hideTooltip() {
|
||||
mutations.hideTooltip();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -234,4 +264,16 @@ export default {
|
||||
.form-flex-group {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.25em !important;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.searchContext {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<!-- Duration Input -->
|
||||
<p>{{ $t('api.tokenDuration') }}</p>
|
||||
<div class="inputWrapper">
|
||||
<div class="sizeInputWrapper">
|
||||
<input class="sizeInput roundedInputLeft input" v-model.number="duration" type="number" min="1"
|
||||
:placeholder="$t('api.durationNumberPlaceholder')" />
|
||||
<select v-model="unit" class="roundedInputRight input">
|
||||
@@ -97,3 +97,8 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.sizeInputWrapper {
|
||||
display: flex !important;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
<p v-else>
|
||||
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
|
||||
</p>
|
||||
<div style="display: grid" aria-label="delete-path" class="searchContext">
|
||||
<div style="display: grid" aria-label="delete-path" class="searchContext button">
|
||||
<span v-for="(item, index) in nav" :key="index"> {{ item }} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
|
||||
<div>Source: {{ sourcePath.source }} </div> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div aria-label="filelist-path" class="searchContext">{{$t('search.path')}} {{ sourcePath.path }}</div>
|
||||
<div aria-label="filelist-path" class="searchContext button clickable">{{$t('search.path')}} {{ sourcePath.path }}</div>
|
||||
<ul class="file-list">
|
||||
<li
|
||||
@click="itemClick"
|
||||
|
||||
@@ -50,16 +50,8 @@ export default {
|
||||
try {
|
||||
event.preventDefault();
|
||||
if (this.name === "") return;
|
||||
await filesApi.post(
|
||||
state.req.source,
|
||||
state.req.path + "/" + this.name,
|
||||
"",
|
||||
true
|
||||
);
|
||||
goToItem(
|
||||
state.req.source,
|
||||
state.req.path + "/" + encodeURIComponent(this.name)
|
||||
);
|
||||
await filesApi.post(state.req.source,state.req.path + "/" + this.name, "", true );
|
||||
goToItem(state.req.source, state.req.path + "/" + this.name);
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("general.password") }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div v-if="showWrongCredentials" class="share__wrong__password">
|
||||
{{ $t("login.wrongCredentials") }}
|
||||
</div>
|
||||
<div class="form-flex-group">
|
||||
<input
|
||||
class="input share-password"
|
||||
v-focus
|
||||
type="password"
|
||||
:placeholder="$t('general.password')"
|
||||
v-model="password"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="submit"
|
||||
:aria-label="$t('buttons.submit')"
|
||||
:title="$t('buttons.submit')"
|
||||
>
|
||||
{{ $t("buttons.submit") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mutations } from "@/store";
|
||||
|
||||
export default {
|
||||
name: "password",
|
||||
props: {
|
||||
submitCallback: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
showWrongCredentials: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
initialPassword: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
password: this.initialPassword,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
mutations.closeHovers();
|
||||
this.submitCallback(this.password);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.share__wrong__password {
|
||||
color: #ff4757;
|
||||
text-align: center;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.share-password {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -30,6 +30,7 @@ import UserEdit from "./UserEdit.vue";
|
||||
import buttons from "@/utils/buttons";
|
||||
import Totp from "./Totp.vue";
|
||||
import Access from "./Access.vue";
|
||||
import Password from "./Password.vue";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
export default {
|
||||
@@ -56,6 +57,7 @@ export default {
|
||||
CreateApi,
|
||||
ActionApi,
|
||||
Access,
|
||||
Password,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.renameMessage", { filename: oldName() }) }}</p>
|
||||
<p>{{ $t("prompts.renameMessage", { filename: oldName }) }}</p>
|
||||
<input class="input" :class="{ 'form-invalid': !validateFileName(name) }" v-focus type="text"
|
||||
@keyup.enter="submit" v-model.trim="name" />
|
||||
<p v-if="!validateFileName(name) && name.length > 0" class="validation-error">
|
||||
@@ -25,40 +25,35 @@
|
||||
</template>
|
||||
<script>
|
||||
import { filesApi } from "@/api";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { mutations } from "@/store";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "rename",
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({ source: "", path: "", name: "" })
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: "",
|
||||
name: this.item?.name || "",
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.name = this.oldName();
|
||||
},
|
||||
computed: {
|
||||
req() {
|
||||
return state.req;
|
||||
},
|
||||
selected() {
|
||||
return state.selected;
|
||||
},
|
||||
selectedCount() {
|
||||
return state.selectedCount;
|
||||
},
|
||||
isListing() {
|
||||
return getters.isListing();
|
||||
},
|
||||
closeHovers() {
|
||||
return mutations.closeHovers;
|
||||
},
|
||||
currentPrompt() {
|
||||
return getters.currentPrompt();
|
||||
oldName() {
|
||||
return this.item?.name || "";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
validateFileName(value) {
|
||||
if (value === "") {
|
||||
return false;
|
||||
@@ -67,25 +62,6 @@ export default {
|
||||
const forbiddenChars = /[/\\]/;
|
||||
return !forbiddenChars.test(value);
|
||||
},
|
||||
cancel() {
|
||||
mutations.closeHovers();
|
||||
},
|
||||
oldName() {
|
||||
// Check if this is being called from upload context with props
|
||||
if (this.currentPrompt && this.currentPrompt.props && this.currentPrompt.props.folderName) {
|
||||
return this.currentPrompt.props.folderName;
|
||||
}
|
||||
|
||||
if (!this.isListing) {
|
||||
return state.req.name;
|
||||
}
|
||||
|
||||
if (getters.selectedCount() === 0 || getters.selectedCount() > 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return state.req.items[this.selected[0]].name;
|
||||
},
|
||||
async submit() {
|
||||
// Validate before submitting
|
||||
if (!this.validateFileName(this.name) || this.name.length === 0) {
|
||||
@@ -94,27 +70,15 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if this is being called from upload context with custom confirm handler
|
||||
if (this.currentPrompt && this.currentPrompt.confirm) {
|
||||
// This is for upload rename - call the custom confirm handler
|
||||
this.currentPrompt.confirm(this.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default file rename operation
|
||||
const items = [{
|
||||
from: state.req.path + "/" + state.req.items[this.selected[0]].name,
|
||||
fromSource: state.req.source,
|
||||
to: state.req.path + "/" + this.name,
|
||||
toSource: state.req.source,
|
||||
from: this.item.path,
|
||||
fromSource: this.item.source,
|
||||
to: this.item.path.replace(/[^/]+$/, this.name),
|
||||
toSource: this.item.source,
|
||||
}];
|
||||
|
||||
await filesApi.moveCopy(items, "move");
|
||||
mutations.closeHovers();
|
||||
if (!this.isListing) {
|
||||
this.$router.push({ path: newLink });
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h2>{{ $t("buttons.share") }}</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div aria-label="share-path" class="searchContext"> {{ $t('search.path') }} {{ subpath }}</div>
|
||||
<div aria-label="share-path" class="searchContext button"> {{ $t('search.path') }} {{ item.path }}</div>
|
||||
<p> {{ $t('share.notice') }} </p>
|
||||
|
||||
<div v-if="listing">
|
||||
@@ -75,13 +75,12 @@
|
||||
</i>
|
||||
</p>
|
||||
<input class="input" type="password" autocomplete="new-password" v-model.trim="password" />
|
||||
|
||||
<div class="settings-items">
|
||||
<div class="settings-items" style="margin-top: 0.5em;">
|
||||
<!--
|
||||
<ToggleSwitch class="item" v-model="allowEdit" :name="'Allow modifications'" />
|
||||
<ToggleSwitch class="item" v-model="allowUpload" :name="'Allow uploading'" />
|
||||
<ToggleSwitch class="item" v-model="disablingFileViewer" :name="'Disable File Viewer'" />
|
||||
-->
|
||||
<ToggleSwitch class="item" v-model="disableFileViewer" :name="'Disable File Viewer'" />
|
||||
<ToggleSwitch
|
||||
class="item"
|
||||
v-model="quickDownload"
|
||||
@@ -136,6 +135,8 @@
|
||||
<ToggleSwitch class="item" v-model="keepAfterExpiration" :name="$t('share.keepAfterExpiration')" :description="$t('share.keepAfterExpirationDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableThumbnails" :name="$t('share.disableThumbnails')" :description="$t('share.disableThumbnailsDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableNavButtons" :name="$t('share.hideNavButtons')" :description="$t('share.hideNavButtonsDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableShareCard" :name="$t('share.disableShareCard')" :description="$t('share.disableShareCardDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableSidebar" :name="$t('share.disableSidebar')" :description="$t('share.disableSidebarDescription')" />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
@@ -252,34 +253,12 @@ import { state, getters, mutations } from "@/store";
|
||||
import { publicApi } from "@/api";
|
||||
import Clipboard from "clipboard";
|
||||
import { fromNow } from "@/utils/moment";
|
||||
import { buildItemUrl } from "@/utils/url";
|
||||
import { buildItemUrl, fixDownloadURL } from "@/utils/url";
|
||||
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
|
||||
import { userSelectableThemes } from "@/utils/constants";
|
||||
import { userSelectableThemes, externalUrl } from "@/utils/constants";
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
//import ViewMode from "@/components/settings/ViewMode.vue";
|
||||
|
||||
/**
|
||||
* @typedef {import('@/api/public').Share} Share
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} FilebrowserFile
|
||||
* @property {string} name
|
||||
* @property {string} path
|
||||
* @property {string} source
|
||||
* @property {boolean} isDir
|
||||
* @property {string} type
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} FilebrowserRequest
|
||||
* @property {FilebrowserFile[]} items
|
||||
* @property {number} numDirs
|
||||
* @property {number} numFiles
|
||||
* @property {{by: string, asc: boolean}} sorting
|
||||
* @property {string} path
|
||||
* @property {string} source
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: "share",
|
||||
components: {
|
||||
@@ -295,6 +274,15 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
path: "",
|
||||
source: "",
|
||||
isDir: false,
|
||||
type: "",
|
||||
}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -304,8 +292,6 @@ export default {
|
||||
links: [],
|
||||
/** @type {Clipboard | null} */
|
||||
clip: null,
|
||||
subpath: "",
|
||||
source: "",
|
||||
password: "",
|
||||
listing: true,
|
||||
allowEdit: false,
|
||||
@@ -314,7 +300,7 @@ export default {
|
||||
disableAnonymous: false,
|
||||
allowUpload: false,
|
||||
maxBandwidth: "",
|
||||
disablingFileViewer: false,
|
||||
disableFileViewer: false,
|
||||
disableThumbnails: false,
|
||||
enableAllowedUsernames: false,
|
||||
allowedUsernames: "",
|
||||
@@ -327,6 +313,8 @@ export default {
|
||||
showAdvanced: false,
|
||||
quickDownload: false,
|
||||
disableNavButtons: false,
|
||||
disableShareCard: false,
|
||||
disableSidebar: false,
|
||||
//viewMode: "normal",
|
||||
};
|
||||
},
|
||||
@@ -389,6 +377,7 @@ export default {
|
||||
this.shareTheme = this.link.shareTheme || "default";
|
||||
this.disableAnonymous = this.link.disableAnonymous || false;
|
||||
this.disableThumbnails = this.link.disableThumbnails || false;
|
||||
this.disableFileViewer = this.link.disableFileViewer || false;
|
||||
this.enableAllowedUsernames = Array.isArray(this.link.allowedUsernames) && this.link.allowedUsernames.length > 0;
|
||||
this.allowedUsernames = this.enableAllowedUsernames ? this.link.allowedUsernames.join(", ") : "";
|
||||
this.keepAfterExpiration = this.link.keepAfterExpiration || false;
|
||||
@@ -399,35 +388,20 @@ export default {
|
||||
this.favicon = this.link.favicon || "";
|
||||
this.quickDownload = this.link.quickDownload || false;
|
||||
this.disableNavButtons = this.link.hideNavButtons || false;
|
||||
this.viewMode = this.link.viewMode || "normal";
|
||||
this.disableShareCard = this.link.disableShareCard || false;
|
||||
this.disableSidebar = this.link.disableSidebar || false;
|
||||
//this.viewMode = this.link.viewMode || "normal";
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
async beforeMount() {
|
||||
if (this.isEditMode) {
|
||||
this.subpath = this.link.path;
|
||||
this.source = this.link.source;
|
||||
return;
|
||||
}
|
||||
let path = this.req.path;
|
||||
this.source = this.req.source;
|
||||
if (state.isSearchActive) {
|
||||
const file = /** @type {FilebrowserFile} */ (this.selected[0]);
|
||||
path = file.path;
|
||||
this.source = file.source;
|
||||
} else if (this.selectedCount === 1) {
|
||||
const index = /** @type {number} */ (this.selected[0]);
|
||||
const selected = this.req.items[index];
|
||||
path = selected.path;
|
||||
this.source = selected.source;
|
||||
}
|
||||
// double encode # to fix issue with # in path
|
||||
// replace all # with %23
|
||||
this.subpath = path.replace(/#/g, "%23");
|
||||
try {
|
||||
// get last element of the path
|
||||
const links = await publicApi.get(this.subpath, this.source);
|
||||
const links = await publicApi.get(this.item.path, this.item.source);
|
||||
this.links = links;
|
||||
} catch (err) {
|
||||
notify.showError(err);
|
||||
@@ -501,20 +475,25 @@ export default {
|
||||
},
|
||||
async submit() {
|
||||
try {
|
||||
if (!this.description) {
|
||||
this.description = this.$t("share.descriptionDefault");
|
||||
}
|
||||
if (!this.title) {
|
||||
this.title = this.$t("share.titleDefault", { title: this.item.name || "share" });
|
||||
}
|
||||
let isPermanent = !this.time || this.time === "0";
|
||||
const payload = {
|
||||
path: this.subpath,
|
||||
sourceName: this.source,
|
||||
source: this.source,
|
||||
path: this.item.path,
|
||||
source: this.item.source,
|
||||
password: this.password,
|
||||
expires: isPermanent ? "" : this.time.toString(),
|
||||
unit: this.unit,
|
||||
disableAnonymous: this.disableAnonymous,
|
||||
allowUpload: this.allowUpload,
|
||||
maxBandwidth: this.maxBandwidth,
|
||||
downloadsLimit: this.downloadsLimit,
|
||||
maxBandwidth: this.maxBandwidth ? parseInt(this.maxBandwidth) : 0,
|
||||
downloadsLimit: this.downloadsLimit ? parseInt(this.downloadsLimit) : 0,
|
||||
shareTheme: this.shareTheme,
|
||||
disablingFileViewer: this.disablingFileViewer,
|
||||
disableFileViewer: this.disableFileViewer,
|
||||
disableThumbnails: this.disableThumbnails,
|
||||
allowedUsernames: this.enableAllowedUsernames ? this.allowedUsernames.split(',').map(u => u.trim()) : [],
|
||||
keepAfterExpiration: this.keepAfterExpiration,
|
||||
@@ -526,41 +505,22 @@ export default {
|
||||
favicon: this.favicon,
|
||||
quickDownload: this.quickDownload,
|
||||
hideNavButtons: this.disableNavButtons,
|
||||
disableShareCard: this.disableShareCard,
|
||||
disableSidebar: this.disableSidebar,
|
||||
};
|
||||
if (this.isEditMode) {
|
||||
payload.hash = this.link.hash;
|
||||
}
|
||||
|
||||
const res = await publicApi.create(payload.path, payload.source, {
|
||||
password: payload.password,
|
||||
expires: payload.expires,
|
||||
unit: payload.unit,
|
||||
disableAnonymous: payload.disableAnonymous,
|
||||
allowUpload: payload.allowUpload,
|
||||
maxBandwidth: Number(payload.maxBandwidth) || 0,
|
||||
downloadsLimit: Number(payload.downloadsLimit) || 0,
|
||||
shareTheme: payload.shareTheme,
|
||||
disableFileViewer: payload.disablingFileViewer,
|
||||
disableThumbnails: payload.disableThumbnails,
|
||||
allowedUsernames: payload.allowedUsernames,
|
||||
hash: payload.hash,
|
||||
keepAfterExpiration: payload.keepAfterExpiration,
|
||||
themeColor: payload.themeColor,
|
||||
banner: payload.banner,
|
||||
title: payload.title,
|
||||
description: payload.description,
|
||||
favicon: payload.favicon,
|
||||
quickDownload: payload.quickDownload,
|
||||
hideNavButtons: payload.hideNavButtons,
|
||||
//viewMode: this.viewMode,
|
||||
});
|
||||
const res = await publicApi.create(payload);
|
||||
|
||||
if (!this.isEditMode) {
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
} else {
|
||||
// reload page to see changes
|
||||
window.location.reload();
|
||||
// emit event to reload shares in settings view
|
||||
eventBus.emit('sharesChanged');
|
||||
mutations.closeHovers();
|
||||
}
|
||||
|
||||
this.time = "";
|
||||
@@ -581,6 +541,8 @@ export default {
|
||||
try {
|
||||
await publicApi.remove(link.hash);
|
||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
||||
// emit event to reload shares in settings view
|
||||
eventBus.emit('sharesChanged');
|
||||
if (this.links.length === 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
@@ -601,21 +563,17 @@ export default {
|
||||
return publicApi.getShareURL(share);
|
||||
},
|
||||
hasDownloadLink() {
|
||||
if (state.isSearchActive) {
|
||||
const file = /** @type {FilebrowserFile} */ (this.selected[0]);
|
||||
return file.type !== "directory";
|
||||
}
|
||||
const index = /** @type {number} */ (this.selected[0]);
|
||||
return this.selected.length === 1 && !this.req.items[index].isDir;
|
||||
// Check if we have a single selected item that can be downloaded
|
||||
return !this.item?.isDir;
|
||||
},
|
||||
/**
|
||||
* @param {Share} share
|
||||
*/
|
||||
buildDownloadLink(share) {
|
||||
share.source = this.source;
|
||||
share.path = "/";
|
||||
const index = /** @type {number} */ (this.selected[0]);
|
||||
return publicApi.getDownloadURL(share, [this.req.items[index].name]);
|
||||
if (share.downloadURL && externalUrl == "") {
|
||||
return this.fixDownloadURL(share.downloadURL);
|
||||
}
|
||||
return publicApi.getDownloadURL(share, [this.item.name]);
|
||||
},
|
||||
sort() {
|
||||
this.links = this.links.sort((a, b) => {
|
||||
@@ -632,6 +590,9 @@ export default {
|
||||
|
||||
this.listing = !this.listing;
|
||||
},
|
||||
fixDownloadURL(downloadUrl) {
|
||||
return fixDownloadURL(downloadUrl);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -43,14 +43,17 @@
|
||||
<div v-if="!disableQuickToggles" class="card-wrapper" @mouseleave="hideTooltip">
|
||||
<div class="quick-toggles">
|
||||
<div
|
||||
class="clickable"
|
||||
:class="{ active: user?.singleClick }"
|
||||
@click="toggleClick"
|
||||
@mouseenter="showTooltip($event, $t('index.toggleClick'))"
|
||||
@mouseleave="hideTooltip"
|
||||
v-if="!isInvalidShare"
|
||||
>
|
||||
<i class="material-icons">ads_click</i>
|
||||
</div>
|
||||
<div
|
||||
class="clickable"
|
||||
:class="{ active: user?.darkMode }"
|
||||
@click="toggleDarkMode"
|
||||
@mouseenter="showTooltip($event, $t('index.toggleDark'))"
|
||||
@@ -59,6 +62,7 @@
|
||||
<i class="material-icons">dark_mode</i>
|
||||
</div>
|
||||
<div
|
||||
class="clickable"
|
||||
:class="{ active: isStickySidebar }"
|
||||
@click="toggleSticky"
|
||||
@mouseenter="showTooltip($event, $t('index.toggleSticky'))"
|
||||
@@ -136,7 +140,7 @@
|
||||
|
||||
<script>
|
||||
import * as auth from "@/utils/auth";
|
||||
import { signup, disableExternal, noAuth, loginPage } from "@/utils/constants";
|
||||
import { signup, disableExternal, noAuth, loginPage, shareInfo } from "@/utils/constants";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes.js";
|
||||
@@ -153,11 +157,12 @@ export default {
|
||||
computed: {
|
||||
disableQuickToggles: () => state.user.disableQuickToggles,
|
||||
hasSourceInfo: () => state.sources.hasSourceInfo,
|
||||
hideSidebarFileActions: () => state.user.hideSidebarFileActions,
|
||||
hideSidebarFileActions: () => state.user.hideSidebarFileActions || getters.isInvalidShare(),
|
||||
settingsAllowed: () => !state.user.disableSettings,
|
||||
isSettings: () => getters.isSettings(),
|
||||
isStickySidebar: () => getters.isStickySidebar(),
|
||||
isMobile: () => getters.isMobile(),
|
||||
isInvalidShare: () => getters.isInvalidShare(),
|
||||
isListingView: () => getters.currentView() == "listingView",
|
||||
user: () => (state.user),
|
||||
isDarkMode: () => getters.isDarkMode(),
|
||||
|
||||
@@ -54,7 +54,7 @@ export default {
|
||||
<style>
|
||||
.active-settings {
|
||||
background: var(--primaryColor) !important;
|
||||
color: var(--item-selected) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="share-sidebar">
|
||||
<div class="share-sidebar__content">
|
||||
<ShareInfo
|
||||
<ShareInfoCard
|
||||
v-if="!disableShareCard"
|
||||
:hash="hash"
|
||||
:token="token"
|
||||
:sub-path="subPath"
|
||||
@@ -11,12 +12,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ShareInfo from "@/components/files/ShareInfo.vue";
|
||||
import ShareInfoCard from "@/components/files/ShareInfoCard.vue";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "SidebarShare",
|
||||
components: {
|
||||
ShareInfo,
|
||||
ShareInfoCard,
|
||||
},
|
||||
props: {
|
||||
hash: {
|
||||
@@ -32,6 +34,11 @@ export default {
|
||||
default: "/",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
disableShareCard() {
|
||||
return shareInfo.disableShareCard;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<SidebarSettings v-if="isSettings"></SidebarSettings>
|
||||
<SidebarGeneral v-if="!isSettings"></SidebarGeneral>
|
||||
<SidebarShare v-if="isShare"></SidebarShare>
|
||||
<SidebarShare v-if="isValidShare"></SidebarShare>
|
||||
|
||||
<div class="buffer"></div>
|
||||
<div v-if="!isSettings" class="credits">
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
mutations.setSeenUpdate(localStorage.getItem("seenUpdate"));
|
||||
},
|
||||
computed: {
|
||||
isShare: () => getters.isShare(),
|
||||
isValidShare: () => getters.isValidShare(),
|
||||
releaseUrl: () => updateAvailable,
|
||||
isDarkMode: () => getters.isDarkMode(),
|
||||
isLoggedIn: () => getters.isLoggedIn(),
|
||||
@@ -172,14 +172,6 @@ body.rtl .action {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable:hover {
|
||||
box-shadow: 0 2px 2px #00000024, 0 1px 5px #0000001f, 0 3px 1px -2px #0003;
|
||||
}
|
||||
|
||||
.release-banner {
|
||||
background-color: var(--primarColor);
|
||||
display: flex;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
width: 100%;
|
||||
background: var(--surfaceSecondary);
|
||||
color: var(--textPrimary);
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.input:hover,
|
||||
|
||||
@@ -179,13 +179,13 @@ input.sizeInput:disabled {
|
||||
border-radius: 1em;
|
||||
margin: .5em;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
padding: .25em;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ input.sizeInput:disabled {
|
||||
backdrop-filter: invert(.1);
|
||||
height: 2em;
|
||||
width: 5em;
|
||||
|
||||
}
|
||||
|
||||
.roundedInputLeft {
|
||||
@@ -206,10 +207,10 @@ input.sizeInput:disabled {
|
||||
border-top-right-radius: 0;
|
||||
width: auto;
|
||||
background-color: var(--surfaceSecondary);
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
.roundedInputRight {
|
||||
padding: 1em;
|
||||
border-radius: 1em;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
|
||||
@@ -224,7 +224,7 @@ body.rtl .card-title>*:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card .card-content.full {
|
||||
.card-content.full {
|
||||
padding-bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -240,7 +240,7 @@ body.rtl .card-title>*:first-child {
|
||||
.card h3 {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
margin: 2em 0 1em;
|
||||
margin: 1em 0 1em;
|
||||
}
|
||||
|
||||
.card code {
|
||||
|
||||
@@ -102,9 +102,10 @@ body.rtl #listingView {
|
||||
}
|
||||
|
||||
#listingView.compact .item:hover,
|
||||
#listingView.list .item:hover {
|
||||
#listingView.list .item:hover,
|
||||
.search-entry:hover {
|
||||
width: 98% !important;
|
||||
margin-left: 0.5em !important;
|
||||
margin-left: 1% !important;
|
||||
padding-left: 0.5em !important;
|
||||
}
|
||||
|
||||
@@ -125,6 +126,7 @@ body.rtl #listingView {
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#listingView.gallery .item .gallery-div {
|
||||
@@ -140,8 +142,10 @@ body.rtl #listingView {
|
||||
#listingView.gallery .item .text {
|
||||
text-shadow: 0 0 2px white;
|
||||
pointer-events: none;
|
||||
font-weight: bold;
|
||||
margin-top: -1em;
|
||||
font-weight: 700;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
transform: translateY(6em);
|
||||
}
|
||||
|
||||
#listingView.gallery .item .text .name {
|
||||
@@ -162,6 +166,10 @@ body.rtl #listingView {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#listingView.list .item img,
|
||||
#listingView.compact .item img {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
#listingView.gallery .item img {
|
||||
width: 100%;
|
||||
|
||||
@@ -50,68 +50,7 @@
|
||||
body.rtl .dashboard .row {
|
||||
margin-right: unset;
|
||||
}
|
||||
#search {
|
||||
min-width: unset;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
#search.active {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
#search-input {
|
||||
transition: 1s ease all;
|
||||
}
|
||||
#search.active #search-input {
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0.075);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(6px);
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
#search.active>div {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
#search.active #result {
|
||||
height: 100vh;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#search.active #result>p>i {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
display: table;
|
||||
}
|
||||
|
||||
#search.active #result ul li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .3em 0;
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
#search-input>.action,
|
||||
#search-input>i {
|
||||
margin-right: 0.3em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#result-list {
|
||||
width:100vw !important;
|
||||
max-width: 100vw !important;
|
||||
left: 0;
|
||||
top: 4em;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -346,13 +346,14 @@ body.rtl .card.floating .card-content .file-list {
|
||||
-webkit-box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%);
|
||||
}
|
||||
|
||||
.item:hover,
|
||||
.quick-toggles div:hover,
|
||||
.button-group button:hover,
|
||||
.search-entry:hover,
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable:hover,
|
||||
button:hover,
|
||||
.searchContext:hover,
|
||||
.item.drag-hover{
|
||||
.action:hover,
|
||||
.listing-item.drag-hover {
|
||||
box-shadow: inset 0 -3em 3em rgba(217, 217, 217, 0.211), 0 0 0 2px var(--alt-background) !important;
|
||||
/* Adjust shadow values as needed */
|
||||
transform: scale(1.02);
|
||||
@@ -400,5 +401,4 @@ button:hover,
|
||||
|
||||
.form-dropdown {
|
||||
width: unset !important;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "استئناف الكل",
|
||||
"clearCompleted": "اكتمل المسح",
|
||||
"showMore": "اعرض المزيد",
|
||||
"showLess": "اعرض أقل"
|
||||
"showLess": "اعرض أقل",
|
||||
"openParentFolder": "فتح المجلد الأصل"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
"internal": "لقد حدث خطأ ما.",
|
||||
"notFound": "لا يمكن الوصول لهذا المحتوى.",
|
||||
"connection": "لا يمكن الوصول إلى الخادم"
|
||||
"connection": "لا يمكن الوصول إلى الخادم",
|
||||
"shareNotFound": "حصة غير صالحة أو منتهية الصلاحية."
|
||||
},
|
||||
"files": {
|
||||
"body": "الصفحة",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "مشاركة الإعدادات",
|
||||
"adminOptions": "خيارات المستخدم المسؤول",
|
||||
"shareDuration": "تنتهي صلاحيتها",
|
||||
"searchOptions": "خيارات البحث"
|
||||
"searchOptions": "خيارات البحث",
|
||||
"downloads": "التنزيلات"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "مساعدة",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "عنوان URL أو المسار الذي يمكن للعميل الوصول إليه من قبل العميل - يتم قبول إما المسار المطلق أو مسار الفهرس: على سبيل المثال 'https://domain.com/favicon.png' أو '/path/to/favicon.png'",
|
||||
"hideNavButtons": "إخفاء أزرار التنقل",
|
||||
"hideNavButtonsDescription": "قم بإخفاء أزرار التنقل على شريط التنقل في المشاركة لإضفاء مظهر بسيط.",
|
||||
"viewModeDescription": "التخطيط الافتراضي للصفحة المشتركة."
|
||||
"viewModeDescription": "التخطيط الافتراضي للصفحة المشتركة.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "تم إرسال مشاركة إليك لعرضها أو تنزيلها.",
|
||||
"disableShareCard": "تعطيل بطاقة المشاركة المعطلة",
|
||||
"disableShareCardDescription": "قم بتعطيل بطاقة المشاركة على الصفحة المشتركة في الشريط الجانبي أو في أعلى الصفحة على الهاتف المحمول.",
|
||||
"disableSidebar": "تعطيل الشريط الجانبي",
|
||||
"disableSidebarDescription": "تعطيل الشريط الجانبي في الصفحة المشتركة."
|
||||
},
|
||||
"api": {
|
||||
"title": "مفاتيح واجهة برمجة التطبيقات (API):",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "تبديل نقرة واحدة",
|
||||
"toggleDark": "تبديل الوضع الداكن",
|
||||
"toggleSticky": "تبديل الوضع اللاصق",
|
||||
"of": "من",
|
||||
"used": "مستعمل",
|
||||
"noSources": "لا توجد مصادر متاحة لهذا المستخدم. إما أن تقوم بإضافة مصدر لهذا المستخدم أو تكوين مصدر مع defaultEnabled: true"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "المجلدات",
|
||||
"exit": "الخروج",
|
||||
"permanent": "دائم",
|
||||
"hash": "تجزئة"
|
||||
"hash": "تجزئة",
|
||||
"of": "من"
|
||||
},
|
||||
"access": {
|
||||
"rules": "قواعد الوصول",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "إدارة الوصول",
|
||||
"totalDenied": "مرفوض",
|
||||
"totalAllowed": "المسموح به",
|
||||
"all": "رفض الكل"
|
||||
"all": "رفض الكل",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "يوضح هذا ما يحدث عندما يحاول المستخدم الوصول إلى مسار لا توجد فيه قواعد وصول محددة. تعني \"السماح\" أنه يمكن للمستخدمين الوصول بشكل افتراضي، وتعني \"الرفض\" أنه يتم حظر المستخدمين بشكل افتراضي ما لم يُسمح لهم صراحةً."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "التحميلات والتنزيلات",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Obnovit vše",
|
||||
"clearCompleted": "Vymazání dokončeno",
|
||||
"showMore": "Zobrazit více",
|
||||
"showLess": "Zobrazit méně"
|
||||
"showLess": "Zobrazit méně",
|
||||
"openParentFolder": "Otevření nadřazené složky"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stáhnout soubor",
|
||||
@@ -62,7 +63,8 @@
|
||||
"forbidden": "Nemáte oprávnění k přístupu.",
|
||||
"internal": "Něco se opravdu pokazilo.",
|
||||
"notFound": "Tuto položku nelze najít.",
|
||||
"connection": "Nelze se připojit k serveru."
|
||||
"connection": "Nelze se připojit k serveru.",
|
||||
"shareNotFound": "Neplatná nebo prošlá akcie."
|
||||
},
|
||||
"files": {
|
||||
"body": "Tělo",
|
||||
@@ -158,7 +160,13 @@
|
||||
"shareFaviconDescription": "Adresa URL nebo cesta k faviconě přístupná klientovi -- akceptována je buď absolutní, nebo indexová cesta: např. \"https://domain.com/favicon.png\" nebo \"/path/to/favicon.png\".",
|
||||
"hideNavButtons": "Skrytí navigačních tlačítek",
|
||||
"hideNavButtonsDescription": "Skryjte navigační tlačítka na panelu navigace ve sdíleném okně, abyste vytvořili minimalistický vzhled.",
|
||||
"viewModeDescription": "Výchozí rozložení sdílené stránky."
|
||||
"viewModeDescription": "Výchozí rozložení sdílené stránky.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Byl vám zaslán podíl, který si můžete prohlédnout nebo stáhnout.",
|
||||
"disableShareCard": "Zakázat sdílenou kartu",
|
||||
"disableShareCardDescription": "Zakázat kartu sdílení na sdílené stránce v postranním panelu nebo v horní části stránky na mobilním telefonu.",
|
||||
"disableSidebar": "Zakázat postranní panel",
|
||||
"disableSidebarDescription": "Zakázat postranní panel na sdílené stránce."
|
||||
},
|
||||
"prompts": {
|
||||
"copy": "Kopírovat",
|
||||
@@ -379,7 +387,8 @@
|
||||
"userUpdated": "Uživatel aktualizován!",
|
||||
"username": "Uživatelské jméno",
|
||||
"users": "Uživatelé",
|
||||
"adminOptions": "Možnosti administrátora"
|
||||
"adminOptions": "Možnosti administrátora",
|
||||
"downloads": "Ke stažení na"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Nápověda",
|
||||
@@ -442,7 +451,6 @@
|
||||
"toggleClick": "Přepnout jedno kliknutí",
|
||||
"toggleDark": "Přepnout tmavý režim",
|
||||
"toggleSticky": "Přepnout sticky režim",
|
||||
"of": "z",
|
||||
"used": "použito",
|
||||
"noSources": "Pro tohoto uživatele nejsou dostupné žádné zdroje. Přidejte zdroj pro uživatele nebo nastavte zdroj s defaultEnabled: true"
|
||||
},
|
||||
@@ -484,7 +492,8 @@
|
||||
"folders": "Složky",
|
||||
"exit": "Exit",
|
||||
"permanent": "Permanentní",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "z"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Pravidla přístupu",
|
||||
@@ -503,7 +512,9 @@
|
||||
"added": "Přidáno pravidlo přístupu!",
|
||||
"name": "Název",
|
||||
"accessManagement": "Správa přístupu",
|
||||
"all": "Odmítnout vše"
|
||||
"all": "Odmítnout vše",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "To ukazuje, co se stane, když se uživatel pokusí získat přístup k cestě, kde neexistují žádná specifická pravidla přístupu. \"Povolit\" znamená, že uživatelé mají ve výchozím nastavení přístup, \"Odmítnout\" znamená, že uživatelé jsou ve výchozím nastavení blokováni, pokud není výslovně povoleno."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nepodařilo se inicializovat editor. Prosím, načtěte jej znovu.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Alle zusammenfassen",
|
||||
"clearCompleted": "Abgeschlossene löschen",
|
||||
"showMore": "Mehr anzeigen",
|
||||
"showLess": "Weniger anzeigen"
|
||||
"showLess": "Weniger anzeigen",
|
||||
"openParentFolder": "Übergeordneten Ordner öffnen"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Datei herunterladen",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Sie haben keine Berechtigung dies abzurufen.",
|
||||
"internal": "Etwas ist schief gelaufen.",
|
||||
"notFound": "Dieser Ort kann nicht angezeigt werden.",
|
||||
"connection": "Der Server ist nicht erreichbar."
|
||||
"connection": "Der Server ist nicht erreichbar.",
|
||||
"shareNotFound": "Ungültige oder abgelaufene Aktie."
|
||||
},
|
||||
"files": {
|
||||
"body": "Body",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Einstellungen teilen",
|
||||
"adminOptions": "Optionen für Verwaltungsbenutzer",
|
||||
"shareDuration": "Abgelaufen",
|
||||
"searchOptions": "Suchoptionen"
|
||||
"searchOptions": "Suchoptionen",
|
||||
"downloads": "Herunterladen"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Hilfe",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "Favicon-URL oder -Pfad, auf den der Client zugreifen kann - es wird entweder ein absoluter oder ein Indexpfad akzeptiert: z. B. 'https://domain.com/favicon.png' oder '/path/to/favicon.png'.",
|
||||
"hideNavButtons": "Navigationsschaltflächen ausblenden",
|
||||
"hideNavButtonsDescription": "Blenden Sie die Navigationsschaltflächen auf der Navigationsleiste in der Freigabe aus, um ein minimalistisches Aussehen zu erzielen.",
|
||||
"viewModeDescription": "Standardlayout für die gemeinsame Seite."
|
||||
"viewModeDescription": "Standardlayout für die gemeinsame Seite.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Sie haben eine Aktie zur Ansicht oder zum Download erhalten.",
|
||||
"disableShareCard": "Freigabekarte deaktivieren",
|
||||
"disableShareCardDescription": "Deaktivieren Sie die Freigabekarte auf der freigegebenen Seite in der Seitenleiste oder oben auf der Seite auf dem Handy.",
|
||||
"disableSidebar": "Seitenleiste deaktivieren",
|
||||
"disableSidebarDescription": "Deaktivieren Sie die Seitenleiste auf der gemeinsamen Seite."
|
||||
},
|
||||
"api": {
|
||||
"title": "API-Schlüssel:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Einzelklick umschalten",
|
||||
"toggleDark": "Dunkelmodus umschalten",
|
||||
"toggleSticky": "Sticky-Modus umschalten",
|
||||
"of": "von",
|
||||
"used": "belegt",
|
||||
"noSources": "Für diesen Benutzer sind keine Quellen verfügbar. Entweder eine Quelle für diesen Benutzer hinzufügen oder eine Quelle mit defaultEnabled: true konfigurieren"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Ordner",
|
||||
"exit": "Ausfahrt",
|
||||
"permanent": "Dauerhaft",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "von"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Zugangsregeln",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Zugangsverwaltung",
|
||||
"totalDenied": "Verweigert",
|
||||
"totalAllowed": "Erlaubt",
|
||||
"all": "Alle ablehnen"
|
||||
"all": "Alle ablehnen",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Dies zeigt, was passiert, wenn ein Benutzer versucht, auf einen Pfad zuzugreifen, für den keine spezifischen Zugriffsregeln bestehen. Zulassen\" bedeutet, dass Benutzer standardmäßig Zugang haben, \"Verweigern\" bedeutet, dass Benutzer standardmäßig blockiert werden, wenn sie nicht ausdrücklich zugelassen sind."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Uploads & Downloads",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Συνέχισε όλα",
|
||||
"clearCompleted": "Εκκαθάριση ολοκληρώθηκε",
|
||||
"showMore": "Εμφάνιση περισσότερων",
|
||||
"showLess": "Εμφάνιση λιγότερων"
|
||||
"showLess": "Εμφάνιση λιγότερων",
|
||||
"openParentFolder": "Άνοιγμα γονικού φακέλου"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Λήψη αρχείου",
|
||||
@@ -62,7 +63,8 @@
|
||||
"forbidden": "Δεν έχετε άδεια πρόσβασης σε αυτό.",
|
||||
"internal": "Προέκυψε εσωτερικό σφάλμα.",
|
||||
"notFound": "Αυτή η τοποθεσία δεν μπορεί να βρεθεί.",
|
||||
"connection": "Ο διακομιστής δεν είναι διαθέσιμος."
|
||||
"connection": "Ο διακομιστής δεν είναι διαθέσιμος.",
|
||||
"shareNotFound": "Άκυρη ή ληγμένη μετοχή."
|
||||
},
|
||||
"files": {
|
||||
"body": "Περιεχόμενο",
|
||||
@@ -300,7 +302,8 @@
|
||||
"shareSettings": "Ρυθμίσεις κοινής χρήσης",
|
||||
"adminOptions": "Επιλογές χρήστη διαχειριστή",
|
||||
"shareDuration": "Λήγει",
|
||||
"searchOptions": "Επιλογές αναζήτησης"
|
||||
"searchOptions": "Επιλογές αναζήτησης",
|
||||
"downloads": "Λήψεις"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Βοήθεια",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "Διεύθυνση URL ή διαδρομή Favicon προσβάσιμη από τον πελάτη -- γίνονται δεκτές είτε η απόλυτη διαδρομή είτε η διαδρομή ευρετηρίου: π.χ. 'https://domain.com/favicon.png' ή '/path/to/favicon.png'.",
|
||||
"hideNavButtons": "Απόκρυψη κουμπιών πλοήγησης",
|
||||
"hideNavButtonsDescription": "Κρύψτε τα κουμπιά πλοήγησης στη γραμμή πλοήγησης στο μερίδιο για να δημιουργήσετε μια μινιμαλιστική εμφάνιση.",
|
||||
"viewModeDescription": "Προεπιλεγμένη διάταξη για την κοινή σελίδα."
|
||||
"viewModeDescription": "Προεπιλεγμένη διάταξη για την κοινή σελίδα.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Σας έχει αποσταλεί ένα share για να το δείτε ή να το κατεβάσετε.",
|
||||
"disableShareCard": "Απενεργοποίηση κάρτας κοινής χρήσης",
|
||||
"disableShareCardDescription": "Απενεργοποιήστε την κάρτα κοινής χρήσης στην κοινόχρηστη σελίδα στην πλαϊνή μπάρα ή στην κορυφή της σελίδας στο κινητό.",
|
||||
"disableSidebar": "Απενεργοποίηση πλευρικής μπάρας",
|
||||
"disableSidebarDescription": "Απενεργοποιήστε την πλαϊνή μπάρα στην κοινή σελίδα."
|
||||
},
|
||||
"api": {
|
||||
"title": "Κλειδιά API:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Εναλλαγή με ένα κλικ",
|
||||
"toggleDark": "Εναλλαγή σκοτεινής λειτουργίας",
|
||||
"toggleSticky": "Εναλλαγή της λειτουργίας Sticky",
|
||||
"of": "του",
|
||||
"used": "χρησιμοποιημένο",
|
||||
"noSources": "Δεν υπάρχουν διαθέσιμες πηγές για αυτόν τον χρήστη. Είτε προσθέστε μια πηγή για αυτόν τον χρήστη είτε ρυθμίστε μια πηγή με defaultEnabled: true"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Φάκελοι",
|
||||
"exit": "Έξοδος",
|
||||
"permanent": "Μόνιμο",
|
||||
"hash": "Κατακερματισμός"
|
||||
"hash": "Κατακερματισμός",
|
||||
"of": "του"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Κανόνες πρόσβασης",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Διαχείριση πρόσβασης",
|
||||
"totalDenied": "Απορρίπτεται",
|
||||
"totalAllowed": "Επιτρεπόμενο",
|
||||
"all": "Άρνηση όλων"
|
||||
"all": "Άρνηση όλων",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Αυτό δείχνει τι συμβαίνει όταν ένας χρήστης προσπαθεί να αποκτήσει πρόσβαση σε μια διαδρομή όπου δεν υπάρχουν συγκεκριμένοι κανόνες πρόσβασης. 'Allow' σημαίνει ότι οι χρήστες μπορούν να έχουν πρόσβαση από προεπιλογή, 'Deny' σημαίνει ότι οι χρήστες αποκλείονται από προεπιλογή, εκτός αν επιτρέπεται ρητά."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Ανεβάσματα & λήψεις",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Resume all",
|
||||
"clearCompleted": "Clear completed",
|
||||
"showMore": "Show more",
|
||||
"showLess": "Show less"
|
||||
"showLess": "Show less",
|
||||
"openParentFolder": "Open parent folder"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -62,7 +63,8 @@
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
"internal": "Something really went wrong.",
|
||||
"notFound": "This location can't be reached.",
|
||||
"connection": "The server can't be reached."
|
||||
"connection": "The server can't be reached.",
|
||||
"shareNotFound": "Invalid or expired share."
|
||||
},
|
||||
"files": {
|
||||
"body": "Body",
|
||||
@@ -158,7 +160,13 @@
|
||||
"shareFaviconDescription": "Favicon URL or path accessible by the client -- either abosolute or index path are accepted: eg 'https://domain.com/favicon.png' or '/path/to/favicon.png'",
|
||||
"hideNavButtons": "Hide navigation buttons",
|
||||
"hideNavButtonsDescription": "Hide the navigation buttons on the navbar in the share to create a minimalistic look.",
|
||||
"viewModeDescription": "Default layout for the shared page."
|
||||
"viewModeDescription": "Default layout for the shared page.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "A share has been sent to you to view or download.",
|
||||
"disableShareCard": "Disable share card",
|
||||
"disableShareCardDescription": "Disable the share card on the shared page in the sidebar or at the top of the page on mobile.",
|
||||
"disableSidebar": "Disable sidebar",
|
||||
"disableSidebarDescription": "Disable the sidebar on the shared page."
|
||||
},
|
||||
"prompts": {
|
||||
"downloadsLimit": "Number of downloads limit",
|
||||
@@ -361,6 +369,7 @@
|
||||
"scope": "Scope",
|
||||
"settingsUpdated": "Settings updated!",
|
||||
"shareDuration": "Expires",
|
||||
"downloads": "Downloads",
|
||||
"shareManagement": "Share Management",
|
||||
"shareDeleted": "Share deleted!",
|
||||
"singleClick": "Use single clicks to open files and directories",
|
||||
@@ -442,7 +451,6 @@
|
||||
"toggleClick": "Toggle Single Click",
|
||||
"toggleDark": "Toggle Dark Mode",
|
||||
"toggleSticky": "Toggle Sticky Mode",
|
||||
"of": "of",
|
||||
"used": "used",
|
||||
"noSources": "No sources available for this user. Either add a source for this user or configure a source with defaultEnabled: true"
|
||||
},
|
||||
@@ -469,7 +477,8 @@
|
||||
"folders": "Folders",
|
||||
"exit": "Exit",
|
||||
"permanent": "Permanent",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "of"
|
||||
},
|
||||
"access": {
|
||||
"all": "Deny All",
|
||||
@@ -488,7 +497,9 @@
|
||||
"deleted": "Access rule deleted!",
|
||||
"added": "Access rule added!",
|
||||
"name": "Name",
|
||||
"accessManagement": "Access Management"
|
||||
"accessManagement": "Access Management",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "This shows what happens when a user tries to access a path where no specific access rules exist. 'Allow' means users can access by default, 'Deny' means users are blocked by default unless explicitly allowed."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Uploads & Downloads",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Reanudar todo",
|
||||
"clearCompleted": "Limpiar lista",
|
||||
"showMore": "Ver más",
|
||||
"showLess": "Mostrar menos"
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir carpeta principal"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descargar archivo",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "No tienes permisos para acceder.",
|
||||
"internal": "Error interno :(",
|
||||
"notFound": "No se encontró el directorio",
|
||||
"connection": "No se puede conectar al servidor."
|
||||
"connection": "No se puede conectar al servidor.",
|
||||
"shareNotFound": "Acción no válida o caducada."
|
||||
},
|
||||
"files": {
|
||||
"body": "Contenido",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Compartidos",
|
||||
"adminOptions": "Opciones de administrador",
|
||||
"shareDuration": "Expira en",
|
||||
"searchOptions": "Opciones de búsqueda"
|
||||
"searchOptions": "Opciones de búsqueda",
|
||||
"downloads": "Descargas"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Ayuda",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "URL del favicon o ruta accesible por el cliente -- se aceptan rutas abosolutas o de índice: por ejemplo 'https://domain.com/favicon.png' o '/ruta/a/favicon.png'.",
|
||||
"hideNavButtons": "Ocultar botones de navegación",
|
||||
"hideNavButtonsDescription": "Oculte los botones de navegación de la barra de navegación en la acción para crear un aspecto minimalista.",
|
||||
"viewModeDescription": "Diseño por defecto de la página compartida."
|
||||
"viewModeDescription": "Diseño por defecto de la página compartida.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Se le ha enviado una acción para que la vea o descargue.",
|
||||
"disableShareCard": "Desactivar tarjeta compartida",
|
||||
"disableShareCardDescription": "Desactivar la tarjeta de compartir en la página compartida en la barra lateral o en la parte superior de la página en móvil.",
|
||||
"disableSidebar": "Desactivar la barra lateral",
|
||||
"disableSidebarDescription": "Desactivar la barra lateral en la página compartida."
|
||||
},
|
||||
"api": {
|
||||
"title": "Crear API:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Abrir con un solo clic",
|
||||
"toggleDark": "Activar modo oscuro",
|
||||
"toggleSticky": "Barra lateral visible",
|
||||
"of": "de",
|
||||
"used": "usado",
|
||||
"noSources": "No hay fuentes disponibles. Añada una o active defaultEnabled: true."
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Carpetas",
|
||||
"exit": "Salida",
|
||||
"permanent": "Permanente",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "de"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Normas de acceso",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Gestión de accesos",
|
||||
"totalDenied": "Denegado",
|
||||
"totalAllowed": "Permitido",
|
||||
"all": "Denegar todo"
|
||||
"all": "Denegar todo",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Muestra lo que ocurre cuando un usuario intenta acceder a una ruta en la que no existen reglas de acceso específicas. Permitir\" significa que los usuarios pueden acceder por defecto, \"Denegar\" significa que los usuarios están bloqueados por defecto a menos que se les permita explícitamente."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Carga de archivos",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Curriculum vitae",
|
||||
"clearCompleted": "Effacement terminé",
|
||||
"showMore": "Afficher plus",
|
||||
"showLess": "Montrer moins"
|
||||
"showLess": "Montrer moins",
|
||||
"openParentFolder": "Ouvrir le dossier parent"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Télécharger le fichier",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Vous n'avez pas la permission d'accéder à cela.",
|
||||
"internal": "Aïe ! Quelque chose s'est mal passé.",
|
||||
"notFound": "Impossible d'accéder à cet emplacement.",
|
||||
"connection": "Le serveur n'est pas accessible."
|
||||
"connection": "Le serveur n'est pas accessible.",
|
||||
"shareNotFound": "Action non valide ou expirée."
|
||||
},
|
||||
"files": {
|
||||
"body": "Corps",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Paramètres des partages",
|
||||
"adminOptions": "Options de l'utilisateur administrateur",
|
||||
"shareDuration": "Expiration",
|
||||
"searchOptions": "Options de recherche"
|
||||
"searchOptions": "Options de recherche",
|
||||
"downloads": "Téléchargements"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Aide",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "URL du favicon ou chemin d'accès accessible par le client -- les chemins d'accès absolus ou indexés sont acceptés : par exemple \"https://domain.com/favicon.png\" ou \"/path/to/favicon.png\".",
|
||||
"hideNavButtons": "Masquer les boutons de navigation",
|
||||
"hideNavButtonsDescription": "Masquez les boutons de navigation de la barre de navigation dans le partage pour créer une apparence minimaliste.",
|
||||
"viewModeDescription": "Mise en page par défaut de la page partagée."
|
||||
"viewModeDescription": "Mise en page par défaut de la page partagée.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Une action vous a été envoyée pour que vous la consultiez ou la téléchargiez.",
|
||||
"disableShareCard": "Désactiver la carte de partage",
|
||||
"disableShareCardDescription": "Désactiver la carte de partage sur la page partagée dans la barre latérale ou en haut de la page sur mobile.",
|
||||
"disableSidebar": "Désactiver la barre latérale",
|
||||
"disableSidebarDescription": "Désactiver la barre latérale sur la page partagée."
|
||||
},
|
||||
"api": {
|
||||
"title": "Clés API :",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Activation du mode simple clic",
|
||||
"toggleDark": "Mode sombre/clair",
|
||||
"toggleSticky": "Activer/Désactiver le mode persistent",
|
||||
"of": "de",
|
||||
"used": "utilisé",
|
||||
"noSources": "Aucune source n'est disponible pour cet utilisateur. Ajoutez une source pour cet utilisateur ou configurez une source avec defaultEnabled : true"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Dossiers",
|
||||
"exit": "Sortie",
|
||||
"permanent": "Permanent",
|
||||
"hash": "Hachures"
|
||||
"hash": "Hachures",
|
||||
"of": "de"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Règles d'accès",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Gestion de l'accès",
|
||||
"totalDenied": "Refusé",
|
||||
"totalAllowed": "Autorisé",
|
||||
"all": "Refuser tout"
|
||||
"all": "Refuser tout",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Ceci montre ce qui se passe lorsqu'un utilisateur tente d'accéder à un chemin pour lequel il n'existe pas de règles d'accès spécifiques. La mention \"Autoriser\" signifie que les utilisateurs peuvent accéder par défaut, la mention \"Refuser\" signifie que les utilisateurs sont bloqués par défaut, à moins qu'ils n'y soient explicitement autorisés."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Chargements et téléchargements",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "המשך הכל",
|
||||
"clearCompleted": "השלם",
|
||||
"showMore": "הצג עוד",
|
||||
"showLess": "הצג פחות"
|
||||
"showLess": "הצג פחות",
|
||||
"openParentFolder": "פתח את התיקייה הראשית"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "הורד קובץ",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "אין לך הרשאות גישה",
|
||||
"internal": "משהו השתבש",
|
||||
"notFound": "לא ניתן להגיע למיקום זה",
|
||||
"connection": "לא ניתן להגיע לשרת"
|
||||
"connection": "לא ניתן להגיע לשרת",
|
||||
"shareNotFound": "שיתוף לא חוקי או פג תוקף."
|
||||
},
|
||||
"files": {
|
||||
"body": "גוף",
|
||||
@@ -297,7 +299,8 @@
|
||||
"api": "מפתחות API",
|
||||
"adminOptions": "אפשרויות משתמש מנהל",
|
||||
"shareDuration": "תוקף",
|
||||
"searchOptions": "אפשרויות חיפוש"
|
||||
"searchOptions": "אפשרויות חיפוש",
|
||||
"downloads": "הורדות"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "עזרה",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "כתובת URL או נתיב של סמל favicon הנגיש ללקוח - ניתן להשתמש בכתובת מוחלטת או בנתיב אינדקס: לדוגמה, 'https://domain.com/favicon.png' או '/path/to/favicon.png'",
|
||||
"hideNavButtons": "הסתר כפתורי ניווט",
|
||||
"hideNavButtonsDescription": "הסתר את כפתורי הניווט בסרגל הניווט בשיתוף כדי ליצור מראה מינימליסטי.",
|
||||
"viewModeDescription": "פריסת ברירת המחדל עבור הדף המשותף."
|
||||
"viewModeDescription": "פריסת ברירת המחדל עבור הדף המשותף.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "נשלחה אליך קובץ לשיתוף או להורדה.",
|
||||
"disableShareCard": "השבת כרטיס שיתוף",
|
||||
"disableShareCardDescription": "השבת את כרטיס השיתוף בדף המשותף בסרגל הצד או בחלק העליון של הדף במכשירים ניידים.",
|
||||
"disableSidebar": "השבת סרגל צד",
|
||||
"disableSidebarDescription": "השבת את סרגל הצד בדף המשותף."
|
||||
},
|
||||
"api": {
|
||||
"title": "מפתחות API",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "החלף לחיצה אחת",
|
||||
"toggleDark": "הפעל מצב כהה",
|
||||
"toggleSticky": "הפעל/כבה מצב דביק",
|
||||
"of": "של",
|
||||
"used": "משומש",
|
||||
"noSources": "אין מקורות זמינים עבור משתמש זה. הוסף מקור עבור משתמש זה או הגדר מקור עם defaultEnabled: true"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "תיקיות",
|
||||
"exit": "יציאה",
|
||||
"permanent": "קבוע",
|
||||
"hash": "חשיש"
|
||||
"hash": "חשיש",
|
||||
"of": "של"
|
||||
},
|
||||
"access": {
|
||||
"rules": "כללי גישה",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "ניהול גישה",
|
||||
"totalDenied": "נדחה",
|
||||
"totalAllowed": "מותר",
|
||||
"all": "דחה הכל"
|
||||
"all": "דחה הכל",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "זה מראה מה קורה כאשר משתמש מנסה לגשת לנתיב שבו אין כללי גישה ספציפיים. 'אפשר' פירושו שמשתמשים יכולים לגשת כברירת מחדל, 'דחה' פירושו שמשתמשים נחסמים כברירת מחדל, אלא אם כן הותר להם במפורש."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "העלאות והורדות",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Folytassa az összes",
|
||||
"clearCompleted": "Tiszta befejezve",
|
||||
"showMore": "Többet mutasson",
|
||||
"showLess": "Kevesebb mutatása"
|
||||
"showLess": "Kevesebb mutatása",
|
||||
"openParentFolder": "Szülői mappa megnyitása"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Fájl letöltése",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Nincs jogosultsága a hozzáféréshez.",
|
||||
"internal": "Valami nagyon elromlott.",
|
||||
"notFound": "Ez a hely nem érhető el.",
|
||||
"connection": "A kiszolgáló nem érhető el."
|
||||
"connection": "A kiszolgáló nem érhető el.",
|
||||
"shareNotFound": "Érvénytelen vagy lejárt részvény."
|
||||
},
|
||||
"files": {
|
||||
"body": "Törzs",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Beállítások megosztása",
|
||||
"adminOptions": "Admin felhasználói beállítások",
|
||||
"shareDuration": "Lejárat:",
|
||||
"searchOptions": "Keresési lehetőségek"
|
||||
"searchOptions": "Keresési lehetőségek",
|
||||
"downloads": "Letöltések"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Súgó",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "A favicon URL címe vagy az ügyfél által elérhető elérési útvonal -- abszolút vagy index útvonal is elfogadható: pl. 'https://domain.com/favicon.png' vagy '/path/to/favicon.png'.",
|
||||
"hideNavButtons": "Navigációs gombok elrejtése",
|
||||
"hideNavButtonsDescription": "Rejtse el a navigációs gombokat a megosztás navigációs sávján a minimalista megjelenés érdekében.",
|
||||
"viewModeDescription": "A megosztott oldal alapértelmezett elrendezése."
|
||||
"viewModeDescription": "A megosztott oldal alapértelmezett elrendezése.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Megosztást küldtünk Önnek megtekintésre vagy letöltésre.",
|
||||
"disableShareCard": "Megosztási kártya letiltása",
|
||||
"disableShareCardDescription": "Kapcsolja ki a megosztási kártyát a megosztott oldalon az oldalsávban vagy mobilon az oldal tetején.",
|
||||
"disableSidebar": "Oldalsáv kikapcsolása",
|
||||
"disableSidebarDescription": "Kapcsolja ki az oldalsávot a megosztott oldalon."
|
||||
},
|
||||
"api": {
|
||||
"title": "API-kulcsok:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Egyetlen kattintás kapcsolása",
|
||||
"toggleDark": "Sötét üzemmód átkapcsolása",
|
||||
"toggleSticky": "Ragadós mód átkapcsolása",
|
||||
"of": "a",
|
||||
"used": "használt",
|
||||
"noSources": "Ehhez a felhasználóhoz nem állnak rendelkezésre források. Vagy adjon hozzá egy forrást ehhez a felhasználóhoz, vagy konfiguráljon egy forrást a defaultEnabled: true beállítással."
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Mappák",
|
||||
"exit": "Kilépés",
|
||||
"permanent": "Állandó",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "a"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Hozzáférési szabályok",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Hozzáférés-kezelés",
|
||||
"totalDenied": "Elutasítva",
|
||||
"totalAllowed": "Engedélyezett",
|
||||
"all": "Mindent megtagadni"
|
||||
"all": "Mindent megtagadni",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Ez azt mutatja, hogy mi történik, ha egy felhasználó megpróbál hozzáférni egy olyan elérési útvonalhoz, ahol nincsenek speciális hozzáférési szabályok. Az 'Allow' azt jelenti, hogy a felhasználók alapértelmezés szerint hozzáférhetnek, a 'Deny' azt jelenti, hogy a felhasználók alapértelmezés szerint blokkolva vannak, kivéve, ha kifejezetten engedélyezik."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Feltöltések és letöltések",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Riprendete tutti",
|
||||
"clearCompleted": "Completato il lavoro di pulizia",
|
||||
"showMore": "Mostra di più",
|
||||
"showLess": "Mostra meno"
|
||||
"showLess": "Mostra meno",
|
||||
"openParentFolder": "Aprire la cartella madre"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Scarica file",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Non hai i permessi per accedere a questo file.",
|
||||
"internal": "Qualcosa è andato veramente male.",
|
||||
"notFound": "Questo percorso non può essere raggiunto.",
|
||||
"connection": "Il server non è raggiungibile."
|
||||
"connection": "Il server non è raggiungibile.",
|
||||
"shareNotFound": "Azione non valida o scaduta."
|
||||
},
|
||||
"files": {
|
||||
"body": "Contenuto",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Condividi le impostazioni",
|
||||
"adminOptions": "Opzioni dell'utente amministratore",
|
||||
"shareDuration": "Scadenza",
|
||||
"searchOptions": "Opzioni di ricerca"
|
||||
"searchOptions": "Opzioni di ricerca",
|
||||
"downloads": "Scaricamento"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Aiuto",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "URL o percorso della favicon accessibile dal client -- sono accettati sia il percorso assoluto che quello indice: ad esempio 'https://domain.com/favicon.png' o '/path/to/favicon.png'.",
|
||||
"hideNavButtons": "Nascondere i pulsanti di navigazione",
|
||||
"hideNavButtonsDescription": "Nascondere i pulsanti di navigazione sulla navbar nella condivisione per creare un look minimalista.",
|
||||
"viewModeDescription": "Layout predefinito per la pagina condivisa."
|
||||
"viewModeDescription": "Layout predefinito per la pagina condivisa.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "È stata inviata una condivisione da visualizzare o scaricare.",
|
||||
"disableShareCard": "Disattivare la scheda di condivisione",
|
||||
"disableShareCardDescription": "Disattivare la scheda di condivisione sulla pagina condivisa nella barra laterale o nella parte superiore della pagina su mobile.",
|
||||
"disableSidebar": "Disattivare la barra laterale",
|
||||
"disableSidebarDescription": "Disattivare la barra laterale della pagina condivisa."
|
||||
},
|
||||
"api": {
|
||||
"title": "Chiavi API:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Alterna il clic singolo",
|
||||
"toggleDark": "Alterna la modalità scura",
|
||||
"toggleSticky": "Alterna la modalità appiccicosa",
|
||||
"of": "di",
|
||||
"used": "usato",
|
||||
"noSources": "Non ci sono fonti disponibili per questo utente. Aggiungere una fonte per questo utente o configurare una fonte con defaultEnabled: true."
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Cartelle",
|
||||
"exit": "Uscita",
|
||||
"permanent": "Permanente",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "di"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regole di accesso",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Gestione degli accessi",
|
||||
"totalDenied": "Negato",
|
||||
"totalAllowed": "Consentito",
|
||||
"all": "Rifiuta tutto"
|
||||
"all": "Rifiuta tutto",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Questo mostra cosa succede quando un utente cerca di accedere a un percorso per il quale non esistono regole di accesso specifiche. 'Consenti' significa che gli utenti possono accedere per impostazione predefinita, 'Rifiuta' significa che gli utenti sono bloccati per impostazione predefinita, a meno che non siano esplicitamente autorizzati."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Caricamenti e download",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "すべての履歴書",
|
||||
"clearCompleted": "クリア完了",
|
||||
"showMore": "もっと見る",
|
||||
"showLess": "もっと見る"
|
||||
"showLess": "もっと見る",
|
||||
"openParentFolder": "親フォルダを開く"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
"internal": "内部エラーが発生しました。",
|
||||
"notFound": "リソースが見つからなりませんでした。",
|
||||
"connection": "サーバーにアクセスできません。"
|
||||
"connection": "サーバーにアクセスできません。",
|
||||
"shareNotFound": "無効または期限切れの共有。"
|
||||
},
|
||||
"files": {
|
||||
"body": "本文",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "共有設定",
|
||||
"adminOptions": "管理者ユーザーオプション",
|
||||
"shareDuration": "期限切れ",
|
||||
"searchOptions": "検索オプション"
|
||||
"searchOptions": "検索オプション",
|
||||
"downloads": "ダウンロード"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "ヘルプ",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "クライアントからアクセス可能なファビコンのURLまたはパス -- 絶対パスでもインデックスパスでも構いません: 'https://domain.com/favicon.png' または '/path/to/favicon.png' など。",
|
||||
"hideNavButtons": "ナビゲーションボタンを隠す",
|
||||
"hideNavButtonsDescription": "シェア内のナビバーにあるナビゲーションボタンを非表示にして、ミニマルな外観を作る。",
|
||||
"viewModeDescription": "共有ページのデフォルトレイアウト。"
|
||||
"viewModeDescription": "共有ページのデフォルトレイアウト。",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "シェアが送信されましたので、閲覧またはダウンロードしてください。",
|
||||
"disableShareCard": "シェアカードを無効にする",
|
||||
"disableShareCardDescription": "サイドバーの共有ページ、またはモバイルのページ上部にある共有カードを無効にする。",
|
||||
"disableSidebar": "サイドバーを無効にする",
|
||||
"disableSidebarDescription": "共有ページのサイドバーを無効にする。"
|
||||
},
|
||||
"api": {
|
||||
"title": "APIキー:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "トグル・シングルクリック",
|
||||
"toggleDark": "ダークモードの切り替え",
|
||||
"toggleSticky": "スティッキーモードの切り替え",
|
||||
"of": "の",
|
||||
"used": "中古",
|
||||
"noSources": "このユーザが使用できるソースはありません。このユーザのソースを追加するか、defaultEnabled: true でソースを構成します。"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "フォルダ",
|
||||
"exit": "出口",
|
||||
"permanent": "正社員",
|
||||
"hash": "ハッシュ"
|
||||
"hash": "ハッシュ",
|
||||
"of": "の"
|
||||
},
|
||||
"access": {
|
||||
"rules": "アクセス規則",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "アクセス管理",
|
||||
"totalDenied": "却下",
|
||||
"totalAllowed": "可",
|
||||
"all": "すべて拒否"
|
||||
"all": "すべて拒否",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "これは、特定のアクセスルールが存在しないパスにユーザがアクセスしようとしたときに何が起こるかを示している。Allow」はユーザーがデフォルトでアクセスできることを意味し、「Deny」は明示的に許可されない限り、ユーザーがデフォルトでブロックされることを意味する。"
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "アップロードとダウンロード",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "모두 다시 시작",
|
||||
"clearCompleted": "완료 지우기",
|
||||
"showMore": "자세히 보기",
|
||||
"showLess": "덜 보기"
|
||||
"showLess": "덜 보기",
|
||||
"openParentFolder": "상위 폴더 열기"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "파일 다운로드",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "접근 권한이 없습니다.",
|
||||
"internal": "오류가 발생하였습니다.",
|
||||
"notFound": "해당 경로를 찾을 수 없습니다.",
|
||||
"connection": "서버에 연결할 수 없습니다."
|
||||
"connection": "서버에 연결할 수 없습니다.",
|
||||
"shareNotFound": "유효하지 않거나 만료된 공유입니다."
|
||||
},
|
||||
"files": {
|
||||
"body": "본문",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "공유 설정",
|
||||
"adminOptions": "관리자 사용자 옵션",
|
||||
"shareDuration": "만료",
|
||||
"searchOptions": "검색 옵션"
|
||||
"searchOptions": "검색 옵션",
|
||||
"downloads": "다운로드"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "도움말",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "클라이언트가 액세스할 수 있는 파비콘 URL 또는 경로 - 절대 경로 또는 인덱스 경로(예: 'https://domain.com/favicon.png' 또는 '/path/to/favicon.png')가 허용됩니다.",
|
||||
"hideNavButtons": "탐색 버튼 숨기기",
|
||||
"hideNavButtonsDescription": "공유의 탐색 모음에서 탐색 버튼을 숨겨 미니멀한 디자인을 만듭니다.",
|
||||
"viewModeDescription": "공유 페이지의 기본 레이아웃입니다."
|
||||
"viewModeDescription": "공유 페이지의 기본 레이아웃입니다.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "보거나 다운로드할 수 있는 공유물이 전송되었습니다.",
|
||||
"disableShareCard": "공유 카드 비활성화",
|
||||
"disableShareCardDescription": "사이드바의 공유 페이지 또는 모바일의 페이지 상단에서 공유 카드를 비활성화합니다.",
|
||||
"disableSidebar": "사이드바 비활성화",
|
||||
"disableSidebarDescription": "공유 페이지에서 사이드바를 비활성화합니다."
|
||||
},
|
||||
"api": {
|
||||
"title": "API 키:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "클릭 한 번으로 토글",
|
||||
"toggleDark": "다크 모드 전환",
|
||||
"toggleSticky": "고정 모드 토글",
|
||||
"of": "의",
|
||||
"used": "사용됨",
|
||||
"noSources": "이 사용자에 대해 사용할 수 있는 소스가 없습니다. 이 사용자에 대한 소스를 추가하거나 defaultEnabled: true로 소스를 구성하세요."
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "폴더",
|
||||
"exit": "종료",
|
||||
"permanent": "영구",
|
||||
"hash": "해시"
|
||||
"hash": "해시",
|
||||
"of": "의"
|
||||
},
|
||||
"access": {
|
||||
"rules": "액세스 규칙",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "액세스 관리",
|
||||
"totalDenied": "거부됨",
|
||||
"totalAllowed": "허용됨",
|
||||
"all": "모두 거부"
|
||||
"all": "모두 거부",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "사용자가 특정 액세스 규칙이 없는 경로에 액세스하려고 할 때 어떤 일이 발생하는지 보여줍니다. '허용'은 사용자가 기본적으로 액세스할 수 있음을 의미하며, '거부'는 명시적으로 허용되지 않는 한 기본적으로 차단됨을 의미합니다."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "업로드 및 다운로드",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Alles hervatten",
|
||||
"clearCompleted": "Gereed",
|
||||
"showMore": "Meer tonen",
|
||||
"showLess": "Minder tonen"
|
||||
"showLess": "Minder tonen",
|
||||
"openParentFolder": "Bovenliggende map openen"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Bestand downloaden",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "U hebt geen rechten om hier toegang toe te krijgen.",
|
||||
"internal": "Er ging iets mis.",
|
||||
"notFound": "Deze locatie kan niet worden bereikt.",
|
||||
"connection": "De server kan niet worden bereikt."
|
||||
"connection": "De server kan niet worden bereikt.",
|
||||
"shareNotFound": "Ongeldig of verlopen aandeel."
|
||||
},
|
||||
"files": {
|
||||
"body": "Body",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareDeleted": "Deel verwijderd!",
|
||||
"adminOptions": "Opties voor beheerders",
|
||||
"shareDuration": "Verloopt op",
|
||||
"searchOptions": "Zoekopties"
|
||||
"searchOptions": "Zoekopties",
|
||||
"downloads": "Downloads"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Help",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "Favicon URL of pad toegankelijk door de client -- zowel abosolute als index path worden geaccepteerd: bijv. 'https://domain.com/favicon.png' of '/path/to/favicon.png'.",
|
||||
"hideNavButtons": "Navigatieknoppen verbergen",
|
||||
"hideNavButtonsDescription": "Verberg de navigatieknoppen op de navigatiebalk in de share om een minimalistische look te creëren.",
|
||||
"viewModeDescription": "Standaard lay-out voor de gedeelde pagina."
|
||||
"viewModeDescription": "Standaard lay-out voor de gedeelde pagina.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Er is een share naar je verzonden om te bekijken of te downloaden.",
|
||||
"disableShareCard": "Aandelenkaart uitschakelen",
|
||||
"disableShareCardDescription": "Schakel de deelkaart uit op de gedeelde pagina in de zijbalk of bovenaan de pagina op mobiel.",
|
||||
"disableSidebar": "Zijbalk uitschakelen",
|
||||
"disableSidebarDescription": "Schakel de zijbalk op de gedeelde pagina uit."
|
||||
},
|
||||
"api": {
|
||||
"title": "API-sleutels",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Schakelen Enkele klik",
|
||||
"toggleDark": "Donkere modus inschakelen",
|
||||
"toggleSticky": "Kleefmodus schakelen",
|
||||
"of": "van",
|
||||
"used": "tweedehands",
|
||||
"noSources": "Geen bronnen beschikbaar voor deze gebruiker. Voeg een bron toe voor deze gebruiker of configureer een bron met defaultEnabled: true"
|
||||
},
|
||||
@@ -484,7 +492,8 @@
|
||||
"folders": "Mappen",
|
||||
"exit": "Ga naar",
|
||||
"permanent": "Permanent",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "van"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Toegangsregels",
|
||||
@@ -503,7 +512,9 @@
|
||||
"added": "Toegangsregel toegevoegd!",
|
||||
"name": "Naam",
|
||||
"accessManagement": "Toegangsbeheer",
|
||||
"all": "Alles weigeren"
|
||||
"all": "Alles weigeren",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Dit laat zien wat er gebeurt als een gebruiker toegang probeert te krijgen tot een pad waarvoor geen specifieke toegangsregels bestaan. Toestaan' betekent dat gebruikers standaard toegang hebben, 'Weigeren' betekent dat gebruikers standaard worden geblokkeerd tenzij ze expliciet toestemming krijgen."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Het is niet gelukt de editor te initialiseren. Gelieve opnieuw te laden.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Resume all",
|
||||
"clearCompleted": "Czyszczenie zakończone",
|
||||
"showMore": "Pokaż więcej",
|
||||
"showLess": "Pokaż mniej"
|
||||
"showLess": "Pokaż mniej",
|
||||
"openParentFolder": "Otwórz folder nadrzędny"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Pobierz plik",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Nie posiadasz uprawnień potrzebnych, by uzyskać do tego dostęp.",
|
||||
"internal": "Pojawił się poważny problem.",
|
||||
"notFound": "Ten adres nie jest poprawny.",
|
||||
"connection": "Nie można uzyskać dostępu do serwera."
|
||||
"connection": "Nie można uzyskać dostępu do serwera.",
|
||||
"shareNotFound": "Nieprawidłowy lub wygasły udział."
|
||||
},
|
||||
"files": {
|
||||
"body": "Body",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Ustawienia udostępniania",
|
||||
"adminOptions": "Opcje użytkownika administratora",
|
||||
"shareDuration": "Wygasa",
|
||||
"searchOptions": "Opcje wyszukiwania"
|
||||
"searchOptions": "Opcje wyszukiwania",
|
||||
"downloads": "Pliki do pobrania"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Pomoc",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "Adres URL Favicon lub ścieżka dostępna dla klienta - akceptowana jest ścieżka bezwzględna lub indeksowa: np. \"https://domain.com/favicon.png\" lub \"/path/to/favicon.png\".",
|
||||
"hideNavButtons": "Ukryj przyciski nawigacyjne",
|
||||
"hideNavButtonsDescription": "Ukryj przyciski nawigacyjne na pasku nawigacyjnym w folderze udostępnionym, aby uzyskać minimalistyczny wygląd.",
|
||||
"viewModeDescription": "Domyślny układ strony udostępnionej."
|
||||
"viewModeDescription": "Domyślny układ strony udostępnionej.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Udział został wysłany do wyświetlenia lub pobrania.",
|
||||
"disableShareCard": "Wyłączanie karty udostępniania",
|
||||
"disableShareCardDescription": "Wyłącz kartę udostępniania na udostępnianej stronie na pasku bocznym lub u góry strony na urządzeniach mobilnych.",
|
||||
"disableSidebar": "Wyłącz pasek boczny",
|
||||
"disableSidebarDescription": "Wyłącz pasek boczny na udostępnionej stronie."
|
||||
},
|
||||
"api": {
|
||||
"title": "Klucze API:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Przełączanie jednym kliknięciem",
|
||||
"toggleDark": "Przełączanie trybu ciemnego",
|
||||
"toggleSticky": "Przełączanie trybu samoprzylepnego",
|
||||
"of": "z",
|
||||
"used": "używany",
|
||||
"noSources": "Brak dostępnych źródeł dla tego użytkownika. Dodaj źródło dla tego użytkownika lub skonfiguruj źródło z defaultEnabled: true."
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Foldery",
|
||||
"exit": "Wyjście",
|
||||
"permanent": "Na stałe",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "z"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Zasady dostępu",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Zarządzanie dostępem",
|
||||
"totalDenied": "Odmowa",
|
||||
"totalAllowed": "Dozwolone",
|
||||
"all": "Odmów wszystkim"
|
||||
"all": "Odmów wszystkim",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "To pokazuje, co się dzieje, gdy użytkownik próbuje uzyskać dostęp do ścieżki, dla której nie istnieją określone reguły dostępu. \"Zezwól\" oznacza, że użytkownicy mogą uzyskać dostęp domyślnie, \"Odmów\" oznacza, że użytkownicy są domyślnie blokowani, chyba że wyraźnie na to zezwolono."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Przesyłanie i pobieranie",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Retomar tudo",
|
||||
"clearCompleted": "Limpar concluído",
|
||||
"showMore": "Mostrar mais",
|
||||
"showLess": "Mostrar menos"
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir a pasta principal"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Baixar arquivo",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Você não tem permissões para acessar isto.",
|
||||
"internal": "Ops! Algum erro ocorreu.",
|
||||
"notFound": "Ops! Nada foi encontrado.",
|
||||
"connection": "O servidor não pode ser alcançado."
|
||||
"connection": "O servidor não pode ser alcançado.",
|
||||
"shareNotFound": "Compartilhamento inválido ou expirado."
|
||||
},
|
||||
"files": {
|
||||
"body": "Corpo",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Compartilhar configurações",
|
||||
"adminOptions": "Opções do usuário administrador",
|
||||
"shareDuration": "Expirações",
|
||||
"searchOptions": "Opções de pesquisa"
|
||||
"searchOptions": "Opções de pesquisa",
|
||||
"downloads": "Downloads"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Ajuda",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "URL ou caminho do Favicon acessível pelo cliente - são aceitos caminhos abosolutos ou de índice: por exemplo, \"https://domain.com/favicon.png\" ou \"/path/to/favicon.png\".",
|
||||
"hideNavButtons": "Ocultar botões de navegação",
|
||||
"hideNavButtonsDescription": "Oculte os botões de navegação na barra de navegação do compartilhamento para criar uma aparência minimalista.",
|
||||
"viewModeDescription": "Layout padrão para a página compartilhada."
|
||||
"viewModeDescription": "Layout padrão para a página compartilhada.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Um compartilhamento foi enviado a você para visualização ou download.",
|
||||
"disableShareCard": "Desativar cartão compartilhado",
|
||||
"disableShareCardDescription": "Desative o cartão de compartilhamento na página compartilhada na barra lateral ou na parte superior da página no celular.",
|
||||
"disableSidebar": "Desativar a barra lateral",
|
||||
"disableSidebarDescription": "Desative a barra lateral na página compartilhada."
|
||||
},
|
||||
"api": {
|
||||
"title": "Chaves de API:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Alternar clique único",
|
||||
"toggleDark": "Alternar o modo escuro",
|
||||
"toggleSticky": "Alternar o modo de aderência",
|
||||
"of": "de",
|
||||
"used": "usado",
|
||||
"noSources": "Não há fontes disponíveis para esse usuário. Adicione uma fonte para esse usuário ou configure uma fonte com defaultEnabled: true"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Pastas",
|
||||
"exit": "Sair",
|
||||
"permanent": "Permanente",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "de"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regras de acesso",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Gerenciamento de acesso",
|
||||
"totalDenied": "Negado",
|
||||
"totalAllowed": "Permitido",
|
||||
"all": "Negar tudo"
|
||||
"all": "Negar tudo",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Isso mostra o que acontece quando um usuário tenta acessar um caminho em que não existem regras de acesso específicas. \"Permitir\" significa que os usuários podem acessar por padrão, \"Negar\" significa que os usuários são bloqueados por padrão, a menos que sejam explicitamente permitidos."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Uploads e downloads",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Retomar tudo",
|
||||
"clearCompleted": "Limpar concluído",
|
||||
"showMore": "Mostrar mais",
|
||||
"showLess": "Mostrar menos"
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir a pasta principal"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarregar ficheiro",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Não tem permissões para aceder a isto",
|
||||
"internal": "Algo correu bastante mal.",
|
||||
"notFound": "Esta localização não é alcançável.",
|
||||
"connection": "O servidor não pode ser contactado."
|
||||
"connection": "O servidor não pode ser contactado.",
|
||||
"shareNotFound": "Ação inválida ou expirada."
|
||||
},
|
||||
"files": {
|
||||
"body": "Corpo",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareDeleted": "Partilhar eliminado!",
|
||||
"adminOptions": "Opções do utilizador administrador",
|
||||
"shareDuration": "Expirações",
|
||||
"searchOptions": "Opções de pesquisa"
|
||||
"searchOptions": "Opções de pesquisa",
|
||||
"downloads": "Transferências"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Ajuda",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "URL ou caminho do Favicon acessível pelo cliente - são aceites caminhos abosolutos ou de índice: por exemplo, \"https://domain.com/favicon.png\" ou \"/path/to/favicon.png",
|
||||
"hideNavButtons": "Ocultar botões de navegação",
|
||||
"hideNavButtonsDescription": "Oculte os botões de navegação na barra de navegação na partilha para criar um aspeto minimalista.",
|
||||
"viewModeDescription": "Esquema predefinido para a página partilhada."
|
||||
"viewModeDescription": "Esquema predefinido para a página partilhada.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Foi-lhe enviada uma ação para visualizar ou descarregar.",
|
||||
"disableShareCard": "Desativar o cartão de partilha",
|
||||
"disableShareCardDescription": "Desativar o cartão de partilha na página partilhada na barra lateral ou na parte superior da página no telemóvel.",
|
||||
"disableSidebar": "Desativar a barra lateral",
|
||||
"disableSidebarDescription": "Desativar a barra lateral na página partilhada."
|
||||
},
|
||||
"api": {
|
||||
"title": "Chaves da API",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Alternar clique único",
|
||||
"toggleDark": "Alternar o modo escuro",
|
||||
"toggleSticky": "Alternar o modo de aderência",
|
||||
"of": "de",
|
||||
"used": "utilizado",
|
||||
"noSources": "Não existem fontes disponíveis para este utilizador. Adicione uma fonte para este utilizador ou configure uma fonte com defaultEnabled: true"
|
||||
},
|
||||
@@ -484,7 +492,8 @@
|
||||
"folders": "Pastas",
|
||||
"exit": "Sair",
|
||||
"permanent": "Permanente",
|
||||
"hash": "Haxixe"
|
||||
"hash": "Haxixe",
|
||||
"of": "de"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regras de acesso",
|
||||
@@ -503,7 +512,9 @@
|
||||
"added": "Regra de acesso adicionada!",
|
||||
"name": "Nome",
|
||||
"accessManagement": "Gestão do acesso",
|
||||
"all": "Recusar tudo"
|
||||
"all": "Recusar tudo",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Isto mostra o que acontece quando um utilizador tenta aceder a um caminho onde não existem regras de acesso específicas. \"Permitir\" significa que os utilizadores podem aceder por predefinição, \"Negar\" significa que os utilizadores são bloqueados por predefinição, a menos que sejam explicitamente autorizados."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Falha ao inicializar o editor. Por favor, recarregue.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "CV toate",
|
||||
"clearCompleted": "Golire finalizată",
|
||||
"showMore": "Arată mai multe",
|
||||
"showLess": "Arată mai puțin"
|
||||
"showLess": "Arată mai puțin",
|
||||
"openParentFolder": "Deschideți folderul părinte"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarcă fișier",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Nu ai permisiuni sa accesezi asta.",
|
||||
"internal": "Ceva nu a funcționat corect.",
|
||||
"notFound": "Aceasta locație nu poate fi accesată.",
|
||||
"connection": "Serverul nu poate fi accesat."
|
||||
"connection": "Serverul nu poate fi accesat.",
|
||||
"shareNotFound": "Acțiune invalidă sau expirată."
|
||||
},
|
||||
"files": {
|
||||
"body": "Corp",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Setări de partajare",
|
||||
"adminOptions": "Opțiuni utilizator administrator",
|
||||
"shareDuration": "Expiră",
|
||||
"searchOptions": "Opțiuni de căutare"
|
||||
"searchOptions": "Opțiuni de căutare",
|
||||
"downloads": "Descărcări"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Ajutor",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "Favicon URL sau cale accesibilă de către client - sunt acceptate fie calea abosolută, fie calea index: de exemplu \"https://domain.com/favicon.png\" sau \"/path/to/favicon.png",
|
||||
"hideNavButtons": "Ascundeți butoanele de navigare",
|
||||
"hideNavButtonsDescription": "Ascundeți butoanele de navigare de pe bara de navigare din share pentru a crea un aspect minimalist.",
|
||||
"viewModeDescription": "Layout implicit pentru pagina partajată."
|
||||
"viewModeDescription": "Layout implicit pentru pagina partajată.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "V-a fost trimisă o acțiune pentru a o vizualiza sau descărca.",
|
||||
"disableShareCard": "Dezactivați cardul de partajare",
|
||||
"disableShareCardDescription": "Dezactivați cardul de partajare pe pagina partajată în bara laterală sau în partea de sus a paginii pe mobil.",
|
||||
"disableSidebar": "Dezactivați bara laterală",
|
||||
"disableSidebarDescription": "Dezactivați bara laterală de pe pagina partajată."
|
||||
},
|
||||
"api": {
|
||||
"title": "Chei API:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Comutare clic unic",
|
||||
"toggleDark": "Comutarea modului întunecat",
|
||||
"toggleSticky": "Schimbarea modului lipicios",
|
||||
"of": "de",
|
||||
"used": "utilizate",
|
||||
"noSources": "Nu există surse disponibile pentru acest utilizator. Adăugați o sursă pentru acest utilizator sau configurați o sursă cu defaultEnabled: true"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Dosare",
|
||||
"exit": "Ieșire",
|
||||
"permanent": "Permanent",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "de"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Reguli de acces",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Gestionarea accesului",
|
||||
"totalDenied": "Refuzat",
|
||||
"totalAllowed": "Permise",
|
||||
"all": "Refuzați toate"
|
||||
"all": "Refuzați toate",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Aceasta arată ce se întâmplă atunci când un utilizator încearcă să acceseze o cale unde nu există reguli specifice de acces. \"Allow\" înseamnă că utilizatorii pot accesa în mod implicit, \"Deny\" înseamnă că utilizatorii sunt blocați în mod implicit dacă nu li se permite în mod explicit."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Încărcări și descărcări",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Резюме все",
|
||||
"clearCompleted": "Очистка завершена",
|
||||
"showMore": "Показать еще",
|
||||
"showLess": "Показать меньше"
|
||||
"showLess": "Показать меньше",
|
||||
"openParentFolder": "Откройте родительскую папку"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Скачать файл",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "У вас нет прав доступа к этому.",
|
||||
"internal": "Что-то пошло не так.",
|
||||
"notFound": "Неправильная ссылка.",
|
||||
"connection": "Нет подключения к серверу."
|
||||
"connection": "Нет подключения к серверу.",
|
||||
"shareNotFound": "Недействительный или истекший ресурс."
|
||||
},
|
||||
"files": {
|
||||
"body": "Тело",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Поделиться настройками",
|
||||
"adminOptions": "Параметры пользователя администратора",
|
||||
"shareDuration": "Срок действия:",
|
||||
"searchOptions": "Параметры поиска"
|
||||
"searchOptions": "Параметры поиска",
|
||||
"downloads": "Скачать"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Помощь",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "URL-адрес фавикона или путь, доступный клиенту - принимается либо абсолютный, либо индексный путь: например, 'https://domain.com/favicon.png' или '/path/to/favicon.png'",
|
||||
"hideNavButtons": "Скрыть кнопки навигации",
|
||||
"hideNavButtonsDescription": "Спрячьте кнопки навигации на навигационной панели в ресурсе, чтобы создать минималистичный вид.",
|
||||
"viewModeDescription": "Макет по умолчанию для общей страницы."
|
||||
"viewModeDescription": "Макет по умолчанию для общей страницы.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Вам отправлена акция для просмотра или загрузки.",
|
||||
"disableShareCard": "Отключение карты доступа",
|
||||
"disableShareCardDescription": "Отключите карточку общего доступа на странице общего доступа в боковой панели или в верхней части страницы на мобильных устройствах.",
|
||||
"disableSidebar": "Отключить боковую панель",
|
||||
"disableSidebarDescription": "Отключите боковую панель на общей странице."
|
||||
},
|
||||
"api": {
|
||||
"title": "Ключи API:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Переключение одним щелчком",
|
||||
"toggleDark": "Переключение темного режима",
|
||||
"toggleSticky": "Переключение режима липкости",
|
||||
"of": "из",
|
||||
"used": "используется",
|
||||
"noSources": "Для этого пользователя нет доступных источников. Либо добавьте источник для этого пользователя, либо настройте источник с параметром defaultEnabled: true"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Папки",
|
||||
"exit": "Выход",
|
||||
"permanent": "Постоянно",
|
||||
"hash": "Хаш"
|
||||
"hash": "Хаш",
|
||||
"of": "из"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Правила доступа",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Управление доступом",
|
||||
"totalDenied": "Отказано",
|
||||
"totalAllowed": "Разрешено",
|
||||
"all": "Запретить все"
|
||||
"all": "Запретить все",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Здесь показано, что происходит, когда пользователь пытается получить доступ к пути, для которого не существует специальных правил доступа. 'Allow' означает, что пользователи могут получить доступ по умолчанию, 'Deny' означает, что пользователи блокируются по умолчанию, если только они не разрешены явным образом."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Загрузка и скачивание",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Obnoviť všetko",
|
||||
"clearCompleted": "Vyčistiť dokončené",
|
||||
"showMore": "Zobraziť viac",
|
||||
"showLess": "Zobraziť menej"
|
||||
"showLess": "Zobraziť menej",
|
||||
"openParentFolder": "Otvorenie nadradeného priečinka"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stiahnuť súbor",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "You don't have permissions to access this.",
|
||||
"internal": "Something really went wrong.",
|
||||
"notFound": "This location can't be reached.",
|
||||
"connection": "The server can't be reached."
|
||||
"connection": "The server can't be reached.",
|
||||
"shareNotFound": "Neplatná alebo neplatná akcia."
|
||||
},
|
||||
"files": {
|
||||
"body": "Telo",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Zdieľať nastavenia",
|
||||
"adminOptions": "Možnosti používateľa správcu",
|
||||
"shareDuration": "Platnosť končí na",
|
||||
"searchOptions": "Možnosti vyhľadávania"
|
||||
"searchOptions": "Možnosti vyhľadávania",
|
||||
"downloads": "Na stiahnutie"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Pomoc",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "Adresa URL alebo cesta k ikone Favicon, ktorá je prístupná klientovi -- akceptuje sa buď absolútna, alebo indexová cesta: napr. \"https://domain.com/favicon.png\" alebo \"/path/to/favicon.png\".",
|
||||
"hideNavButtons": "Skrytie navigačných tlačidiel",
|
||||
"hideNavButtonsDescription": "Skryte navigačné tlačidlá na paneli navigácie v zdieľaní, aby ste vytvorili minimalistický vzhľad.",
|
||||
"viewModeDescription": "Predvolené rozloženie zdieľanej stránky."
|
||||
"viewModeDescription": "Predvolené rozloženie zdieľanej stránky.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Bola vám odoslaná akcia na zobrazenie alebo stiahnutie.",
|
||||
"disableShareCard": "Zakázanie zdieľanej karty",
|
||||
"disableShareCardDescription": "Zakážte kartu zdieľania na zdieľanej stránke v bočnom paneli alebo v hornej časti stránky na mobilnom zariadení.",
|
||||
"disableSidebar": "Zakázanie bočného panela",
|
||||
"disableSidebarDescription": "Zakážte bočný panel na zdieľanej stránke."
|
||||
},
|
||||
"api": {
|
||||
"title": "Kľúče API:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Prepínanie jedným kliknutím",
|
||||
"toggleDark": "Prepínanie tmavého režimu",
|
||||
"toggleSticky": "Prepnutie režimu Sticky",
|
||||
"of": "z",
|
||||
"used": "použité",
|
||||
"noSources": "Pre tohto používateľa nie sú k dispozícii žiadne zdroje. Buď pridajte zdroj pre tohto používateľa, alebo nakonfigurujte zdroj s defaultEnabled: true"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Priečinky",
|
||||
"exit": "Exit",
|
||||
"permanent": "Trvalé",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "z"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Pravidlá prístupu",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Správa prístupu",
|
||||
"totalDenied": "Odmietnuté",
|
||||
"totalAllowed": "Povolené",
|
||||
"all": "Odmietnuť všetko"
|
||||
"all": "Odmietnuť všetko",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Toto ukazuje, čo sa stane, keď sa používateľ pokúsi získať prístup k ceste, kde neexistujú žiadne špecifické pravidlá prístupu. \"Allow\" (Povoliť) znamená, že používatelia majú predvolený prístup, \"Deny\" (Odmietnuť) znamená, že používatelia sú predvolene blokovaní, pokiaľ to nie je výslovne povolené."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Nahrávanie a sťahovanie",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Återuppta alla",
|
||||
"clearCompleted": "Klart avslutat",
|
||||
"showMore": "Visa mer",
|
||||
"showLess": "Visa mindre"
|
||||
"showLess": "Visa mindre",
|
||||
"openParentFolder": "Öppna överordnad mapp"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Ladda ner fil",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Du saknar rättigheter till detta",
|
||||
"internal": "Något gick fel",
|
||||
"notFound": "Det går inte att nå den här platsen.",
|
||||
"connection": "Det går inte att nå servern."
|
||||
"connection": "Det går inte att nå servern.",
|
||||
"shareNotFound": "Ogiltig eller utgången andel."
|
||||
},
|
||||
"files": {
|
||||
"body": "Huvud",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareDeleted": "Dela borttaget!",
|
||||
"adminOptions": "Alternativ för administratörsanvändare",
|
||||
"shareDuration": "Upphör att gälla",
|
||||
"searchOptions": "Sökalternativ"
|
||||
"searchOptions": "Sökalternativ",
|
||||
"downloads": "Nedladdningar"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Hjälp",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "URL eller sökväg till Favicon som är tillgänglig för klienten - antingen abosolute eller indexsökväg accepteras: t.ex. \"https://domain.com/favicon.png\" eller \"/path/to/favicon.png",
|
||||
"hideNavButtons": "Dölj navigeringsknappar",
|
||||
"hideNavButtonsDescription": "Dölj navigeringsknapparna i navigeringsfältet i aktien för att skapa ett minimalistiskt utseende.",
|
||||
"viewModeDescription": "Standardlayout för den delade sidan."
|
||||
"viewModeDescription": "Standardlayout för den delade sidan.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "En aktie har skickats till dig för visning eller nedladdning.",
|
||||
"disableShareCard": "Inaktivera aktiekort",
|
||||
"disableShareCardDescription": "Inaktivera delningskortet på den delade sidan i sidofältet eller högst upp på sidan på mobilen.",
|
||||
"disableSidebar": "Inaktivera sidofält",
|
||||
"disableSidebarDescription": "Inaktivera sidofältet på den delade sidan."
|
||||
},
|
||||
"api": {
|
||||
"title": "API-nycklar",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Toggle Enkelklick",
|
||||
"toggleDark": "Växla till mörkt läge",
|
||||
"toggleSticky": "Växla klisterläge",
|
||||
"of": "av",
|
||||
"used": "används",
|
||||
"noSources": "Inga källor tillgängliga för den här användaren. Lägg antingen till en källa för den här användaren eller konfigurera en källa med defaultEnabled: true"
|
||||
},
|
||||
@@ -484,7 +492,8 @@
|
||||
"folders": "Mappar",
|
||||
"exit": "Avsluta",
|
||||
"permanent": "Permanent",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "av"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regler för åtkomst",
|
||||
@@ -503,7 +512,9 @@
|
||||
"added": "Tillträdesregel tillagd!",
|
||||
"name": "Namn",
|
||||
"accessManagement": "Hantering av åtkomst",
|
||||
"all": "Avvisa alla"
|
||||
"all": "Avvisa alla",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Detta visar vad som händer när en användare försöker komma åt en sökväg där det inte finns några specifika åtkomstregler. \"Allow\" innebär att användare kan komma åt som standard, \"Deny\" innebär att användare blockeras som standard om de inte uttryckligen tillåts."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Redigeraren kunde inte initieras. Vänligen ladda om.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Tümünü devam ettir",
|
||||
"clearCompleted": "Temizleme tamamlandı",
|
||||
"showMore": "Daha fazla göster",
|
||||
"showLess": "Daha az göster"
|
||||
"showLess": "Daha az göster",
|
||||
"openParentFolder": "Üst klasörü açın"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Dosyayı indir",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "Buna erişim izniniz yok.",
|
||||
"internal": "Bir şeyler ters gitti.",
|
||||
"notFound": "Bu konuma ulaşılamıyor.",
|
||||
"connection": "Sunucuya ulaşılamıyor."
|
||||
"connection": "Sunucuya ulaşılamıyor.",
|
||||
"shareNotFound": "Geçersiz veya süresi dolmuş paylaşım."
|
||||
},
|
||||
"files": {
|
||||
"body": "Sayfa",
|
||||
@@ -297,7 +299,8 @@
|
||||
"shareSettings": "Paylaşım Ayarları",
|
||||
"adminOptions": "Yönetici kullanıcı seçenekleri",
|
||||
"shareDuration": "Sona erdi",
|
||||
"searchOptions": "Arama seçenekleri"
|
||||
"searchOptions": "Arama seçenekleri",
|
||||
"downloads": "İndirmeler"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Yardım",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "Favicon URL'si veya istemci tarafından erişilebilen yol -- mutlak veya dizin yolu kabul edilir: örneğin 'https://domain.com/favicon.png' veya '/path/to/favicon.png'",
|
||||
"hideNavButtons": "Gezinme düğmelerini gizle",
|
||||
"hideNavButtonsDescription": "Minimalist bir görünüm oluşturmak için paylaşımdaki gezinme çubuğundaki gezinme düğmelerini gizleyin.",
|
||||
"viewModeDescription": "Paylaşılan sayfa için varsayılan düzen."
|
||||
"viewModeDescription": "Paylaşılan sayfa için varsayılan düzen.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Görüntülemeniz veya indirmeniz için size bir paylaşım gönderildi.",
|
||||
"disableShareCard": "Paylaşım kartını devre dışı bırak",
|
||||
"disableShareCardDescription": "Kenar çubuğunda paylaşılan sayfadaki veya mobil cihazlarda sayfanın üst kısmındaki paylaşım kartını devre dışı bırakın.",
|
||||
"disableSidebar": "Kenar çubuğunu devre dışı bırak",
|
||||
"disableSidebarDescription": "Paylaşılan sayfadaki kenar çubuğunu devre dışı bırakın."
|
||||
},
|
||||
"api": {
|
||||
"title": "API Anahtarları:",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Tek Tıklamayı Değiştir",
|
||||
"toggleDark": "Karanlık Modu Aç / Kapat",
|
||||
"toggleSticky": "Yapışkan Modunu Değiştir",
|
||||
"of": ".",
|
||||
"used": "kullanılmış",
|
||||
"noSources": "Bu kullanıcı için kullanılabilir kaynak yok. Ya bu kullanıcı için bir kaynak ekleyin ya da defaultEnabled: true ile bir kaynak yapılandırın"
|
||||
},
|
||||
@@ -418,7 +426,8 @@
|
||||
"folders": "Klasörler",
|
||||
"exit": "Çıkış",
|
||||
"permanent": "Daimi",
|
||||
"hash": "Hash"
|
||||
"hash": "Hash",
|
||||
"of": "."
|
||||
},
|
||||
"access": {
|
||||
"rules": "Erişim kuralları",
|
||||
@@ -437,7 +446,9 @@
|
||||
"accessManagement": "Erişim Yönetimi",
|
||||
"totalDenied": "Reddedildi",
|
||||
"totalAllowed": "İzin verildi",
|
||||
"all": "Tümünü Reddet"
|
||||
"all": "Tümünü Reddet",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Bu, bir kullanıcı belirli erişim kurallarının bulunmadığı bir yola erişmeye çalıştığında ne olacağını gösterir. 'Allow' kullanıcıların varsayılan olarak erişebileceği anlamına gelir, 'Deny' ise açıkça izin verilmediği sürece kullanıcıların varsayılan olarak engellendiği anlamına gelir."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Yüklemeler & İndirmeler",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"resumeAll": "Відновити все",
|
||||
"clearCompleted": "Очищення завершено",
|
||||
"showMore": "Показати більше",
|
||||
"showLess": "Показати менше"
|
||||
"showLess": "Показати менше",
|
||||
"openParentFolder": "Відкрийте батьківську папку"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Завантажити файл",
|
||||
@@ -59,7 +60,8 @@
|
||||
"forbidden": "У вас немає прав доступу до цього.",
|
||||
"internal": "Щось пішло не так.",
|
||||
"notFound": "Неправильне посилання.",
|
||||
"connection": "Немає підключення до сервера."
|
||||
"connection": "Немає підключення до сервера.",
|
||||
"shareNotFound": "Недійсна або прострочена акція."
|
||||
},
|
||||
"files": {
|
||||
"body": "Тіло",
|
||||
@@ -297,7 +299,8 @@
|
||||
"api": "Ключі API",
|
||||
"adminOptions": "Параметри користувача адміністрування",
|
||||
"shareDuration": "Закінчується",
|
||||
"searchOptions": "Параметри пошуку"
|
||||
"searchOptions": "Параметри пошуку",
|
||||
"downloads": "Завантаження"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Допомога",
|
||||
@@ -354,7 +357,13 @@
|
||||
"shareFaviconDescription": "URL-адреса іконки або шлях, доступний клієнту - допускається як абсолютний, так і індексний шлях: наприклад, 'https://domain.com/favicon.png' або '/путь/до/favicon.png'.",
|
||||
"hideNavButtons": "Приховати кнопки навігації",
|
||||
"hideNavButtonsDescription": "Приховайте кнопки навігації на панелі навігації у спільному доступі, щоб створити мінімалістичний вигляд.",
|
||||
"viewModeDescription": "Макет за замовчуванням для спільної сторінки."
|
||||
"viewModeDescription": "Макет за замовчуванням для спільної сторінки.",
|
||||
"titleDefault": "Shared files - {title}",
|
||||
"descriptionDefault": "Вам надіслано файл для перегляду або завантаження.",
|
||||
"disableShareCard": "Вимкнути картку спільного доступу",
|
||||
"disableShareCardDescription": "Вимкніть картку спільного доступу на сторінці, до якої надається спільний доступ, на бічній панелі або вгорі сторінки на мобільних пристроях.",
|
||||
"disableSidebar": "Вимкнути бічну панель",
|
||||
"disableSidebarDescription": "Вимкнути бічну панель на спільній сторінці."
|
||||
},
|
||||
"api": {
|
||||
"title": "Ключі API",
|
||||
@@ -391,7 +400,6 @@
|
||||
"toggleClick": "Перемикання одним клацанням миші",
|
||||
"toggleDark": "Увімкнути темний режим",
|
||||
"toggleSticky": "Увімкнути липкий режим",
|
||||
"of": "з",
|
||||
"used": "використаний",
|
||||
"noSources": "Для цього користувача немає доступних джерел. Або додайте джерело для цього користувача, або налаштуйте джерело з defaultEnabled: true"
|
||||
},
|
||||
@@ -484,7 +492,8 @@
|
||||
"folders": "Папки",
|
||||
"exit": "Виходьте.",
|
||||
"permanent": "Постійний",
|
||||
"hash": "Хеш"
|
||||
"hash": "Хеш",
|
||||
"of": "з"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Правила доступу",
|
||||
@@ -503,7 +512,9 @@
|
||||
"added": "Додано правило доступу!",
|
||||
"name": "Ім'я",
|
||||
"accessManagement": "Керування доступом",
|
||||
"all": "Заперечувати все."
|
||||
"all": "Заперечувати все.",
|
||||
"defaultBehavior": "Default action{suffix}",
|
||||
"defaultBehaviorDescription": "Тут показано, що відбувається, коли користувач намагається отримати доступ до шляху, для якого не існує певних правил доступу. \"Дозволити\" означає, що користувачі можуть отримати доступ за замовчуванням, \"Заборонити\" означає, що користувачі за замовчуванням заблоковані, якщо тільки це не дозволено явно."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Не вдалося ініціалізувати редактор. Будь ласка, перезавантажте.",
|
||||
|
||||
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user