Compare commits
12
Commits
main
...
v0.6.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18430db8b2 | ||
|
|
5bcf627f23 | ||
|
|
64ae051bd6 | ||
|
|
f09357ef72 | ||
|
|
07f7e631ed | ||
|
|
8d5337bbd3 | ||
|
|
7103257c7d | ||
|
|
9abe99d80e | ||
|
|
72d7b63db3 | ||
|
|
7a2834b8f6 | ||
|
|
c6a01fbaa8 | ||
|
|
6f2bb22bfe |
No files matched your search
@@ -1,20 +1,10 @@
|
||||
**Description**
|
||||
<!--
|
||||
Please explain the changes you made here.
|
||||
If the feature changes current behaviour, explain why your solution is better.
|
||||
-->
|
||||
|
||||
:rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/).
|
||||
According to the [contributing guide](https://github.com/gtsteffaniak/filebrowser/wiki/Contributing#contributing-as-an-unofficial-contributor), A PR should contain:
|
||||
|
||||
- [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master!
|
||||
- [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*.
|
||||
- [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md).
|
||||
- [ ] DO make sure that related issues are opened in other repositories. I.e., the frontend, caddy plugins or the web page need to be updated accordingly.
|
||||
- [ ] AVOID breaking the continuous integration build.
|
||||
- [ ] A clear description of why it was opened.
|
||||
- [ ] A short title that best describes the change.
|
||||
- [ ] Must pass unit and integration tests, which can be run checked locally prior to opening a PR.
|
||||
- [ ] Any additional details for functionality not covered by tests.
|
||||
|
||||
**Further comments**
|
||||
<!--
|
||||
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc.
|
||||
|
||||
:heart: Thank you!
|
||||
-->
|
||||
**Additional Details**
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile.playwright-regular
|
||||
file: ./_docker/Dockerfile.playwright-general
|
||||
push: false
|
||||
push_latest_to_registry:
|
||||
needs: [ test_playwright ]
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test_playwright:
|
||||
name: Test Playwright
|
||||
name: Test Playwright - regular
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -30,10 +30,10 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile.playwright-regular
|
||||
file: ./_docker/Dockerfile.playwright-general
|
||||
push: false
|
||||
test_playwright_proxy:
|
||||
name: Test Playwright - regular
|
||||
name: Test Playwright - proxy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
file: ./_docker/Dockerfile.playwright-proxy
|
||||
push: false
|
||||
test_playwright_noauth:
|
||||
name: Test Playwright - regular
|
||||
name: Test Playwright - noauth
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile.playwright-regular
|
||||
file: ./_docker/Dockerfile.playwright-general
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_playwright ]
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./_docker/Dockerfile.playwright-regular
|
||||
file: ./_docker/Dockerfile.playwright-general
|
||||
push: false
|
||||
create_release_tag:
|
||||
needs: [ test_playwright ]
|
||||
|
||||
@@ -54,17 +54,14 @@ jobs:
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON='${{ steps.meta.outputs.json }}' # Assuming the JSON output is stored in steps.meta.outputs.json
|
||||
|
||||
# Process the JSON array using jq
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq '
|
||||
.tags |= map(sub(":v"; ":")) |
|
||||
if any(.tags[]; test("beta")) then .tags += ["gtstef/filebrowser:beta"] else . end |
|
||||
if any(.tags[]; test("stable")) then .tags += ["gtstef/filebrowser:stable"] else . end
|
||||
')
|
||||
|
||||
# Convert JSON to a comma-separated string for Docker tags
|
||||
TAGS_CSV=$(echo "$MODIFIED_JSON" | jq -r '.tags | join(",")')
|
||||
|
||||
echo "$TAGS_CSV" > modified_tags.txt # Save for debugging
|
||||
echo "cleaned_tag=$TAGS_CSV" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
+38
-2
@@ -2,6 +2,40 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v0.6.0-beta
|
||||
|
||||
> [!WARNING]
|
||||
> This release includes several config changes that could cause issues. Please backup your database file before upgrading.
|
||||
|
||||
This release has several changes that should work without issues... however, still backup your database file first and proceed with caution. User permissions and source config changes have been updated -- and the `server.root` paramter is no longer used.
|
||||
|
||||
This is a significant step towards a stable release. There shouldn't be any major breaking config changes after this.
|
||||
|
||||
**New Features**:
|
||||
- multiple sources support https://github.com/gtsteffaniak/filebrowser/issues/360
|
||||
- listing view keeps them independant, you switch between the two and the url address will have a prefix `/files/<sourcename>/path/to/file` when there is more than 1 source.
|
||||
- search also happens independantly, with a selection toggle per source. searching current source searches the current scope in the listing view, if you toggle to an alternative source it will search from the source root.
|
||||
- copy/moving is currently only supported within the same source -- that will come in a future release.
|
||||
- `FILEBROWSER_CONFIG` environment variable is respected if no CLI config parameter is provided. https://github.com/gtsteffaniak/filebrowser/issues/413
|
||||
|
||||
**Notes**:
|
||||
- downloads no longer open new window.
|
||||
- swagger updated with auth api help for things like api token.
|
||||
- GET api keys now uses `name` query instead of `key`. eg `GET /api/auth/tokens?name=apikeyname`
|
||||
- user permissions simplified to four permission groups (no config change required):
|
||||
- **removed** : create, rename, delete, download
|
||||
- **remaining**: admin, modify, share, api
|
||||
- `scope` is deprecated, but still supported, applies to default source. if using multiple sources, set `defaultUserScope` at the [source config](https://github.com/gtsteffaniak/filebrowser/wiki/Configuration#default-source-configuration) instead.
|
||||
- **removed** user rules and commands.
|
||||
- commands feature has never been enabled so just removing the references.
|
||||
- rules will come back in a different form (not applied to the user).
|
||||
- `server.root` is completely removed in favor of `server.sources`
|
||||
|
||||
**BugFixes**:
|
||||
- fix conflict resolution issue https://github.com/gtsteffaniak/filebrowser/issues/384
|
||||
- many user creation page bugfixes.
|
||||
- fix share delete issue https://github.com/gtsteffaniak/filebrowser/issues/408
|
||||
|
||||
## v0.5.4-beta
|
||||
|
||||
**BugFixes**:
|
||||
@@ -9,10 +43,12 @@ All notable changes to this project will be documented in this file. For commit
|
||||
- drag and drop on empty folders https://github.com/gtsteffaniak/filebrowser/issues/361
|
||||
- preview navigation issue https://github.com/gtsteffaniak/filebrowser/issues/372
|
||||
- auth proxy password length error https://github.com/gtsteffaniak/filebrowser/issues/375
|
||||
|
||||
|
||||
<img width="294" alt="image" src="https://github.com/user-attachments/assets/669bca75-98d4-47c1-838b-1ffee2967d7d" />
|
||||
|
||||
## v0.5.3-beta
|
||||
|
||||
**New Features**
|
||||
**New Features**:
|
||||
- onlyoffice disable filetypes for user specified file types. https://github.com/gtsteffaniak/filebrowser/issues/346
|
||||
|
||||
**Notes**:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<h3>FileBrowser Quantum</h3>
|
||||
A modern web-based file manager
|
||||
<br/><br/>
|
||||
<img width="800" src="https://github.com/user-attachments/assets/c991fc69-a05b-4f34-b915-0d3cded887a7" title="Main Screenshot">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/c35a8455-c32d-47f5-853d-f285e160b022" title="Main Screenshot">
|
||||
</div>
|
||||
|
||||
> [!WARNING]
|
||||
@@ -25,33 +25,28 @@ FileBrowser Quantum is a fork of the file browser open-source project with the f
|
||||
- Real-time search results as you type
|
||||
- Search supports file/folder sizes and many file type filters.
|
||||
- Enhanced interactive results that show file/folder sizes.
|
||||
2. ✅ Revamped UI
|
||||
2. ✅ Multiple sources support
|
||||
3. ✅ Revamped UI
|
||||
- Additional compact view mode as well as refreshed view mode styles.
|
||||
- Many graphical and user experience improvements.
|
||||
- right-click context menu
|
||||
3. ✅ Revamped and simplified configuration via `config.yaml` config file.
|
||||
4. ✅ Better listing browsing
|
||||
4. ✅ Revamped and simplified configuration via `config.yaml` config file.
|
||||
5. ✅ Better listing browsing
|
||||
- Instantly Switches view modes and sort order without reloading data.
|
||||
- Folder sizes are displayed
|
||||
- Navigating remembers the scroll position, navigating back keeps the last scroll position.
|
||||
5. ✅ Developer API support
|
||||
6. ✅ Developer API support
|
||||
- Ability to create long-live API Tokens.
|
||||
- Helpful Swagger page available at `/swagger` endpoint.
|
||||
|
||||
Notable features that this fork *does not* have (removed):
|
||||
|
||||
- ❌ jobs/runners are not supported yet (planned).
|
||||
- ❌ per-user rules are not supported yet (planned).
|
||||
- ❌ jobs are not supported yet (planned).
|
||||
- ❌ rules are not supported yet (planned).
|
||||
- ❌ shell commands are completely removed and will not be returned.
|
||||
|
||||
## About
|
||||
|
||||
This repository is a fork of the original [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
with a collection of changes that make this program work better in terms of
|
||||
aesthetics and performance. Improved search, simplified UI
|
||||
(without removing features) and more secure and up-to-date
|
||||
build are just a few examples.
|
||||
|
||||
FileBrowser Quantum differs significantly from the original.
|
||||
There are hundreds of thousands of lines changed and they are generally
|
||||
no longer compatible with each other. This has been intentional -- the
|
||||
@@ -77,9 +72,7 @@ action panel. If the action is available based on context, it will show up as
|
||||
a popup menu.
|
||||
|
||||
<p align="center">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/2be7a6c5-0f95-4d9f-bc05-484ee71246d8" title="Search GIF">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/f55a6f1f-b930-4399-98b5-94da6e90527a" title="Navigation GIF">
|
||||
<img width="800" src="https://github.com/user-attachments/assets/93b019de-d38f-4aaa-bde3-3ba4e99ecd25" title="Main Screenshot">
|
||||
<img width="1000" src="https://github.com/user-attachments/assets/aa32b05c-f917-47bb-b07f-857edc5e47f7" title="Search GIF">
|
||||
</p>
|
||||
|
||||
## Install and Configuration
|
||||
@@ -120,7 +113,7 @@ S3 support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
webdav support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
FTP support | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Dedicated docs site? | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ |
|
||||
Multiple sources at once | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Multiple sources at once | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
Docker image size | 31 MB | 31 MB | 240 MB (main image) | 250 MB | ❌ | > 2 GB |
|
||||
Min. Memory Requirements | 128 MB | 128 MB | 128 MB (main image) | 128 MB | ❌ | 4 GB |
|
||||
has standalone binary | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
File renamed without changes.
@@ -7,6 +7,7 @@ services:
|
||||
volumes:
|
||||
- ./src/proxy/backend/default.conf:/etc/nginx/conf.d/default.conf
|
||||
filebrowser:
|
||||
hostname: filebrowser
|
||||
volumes:
|
||||
- '../frontend:/home/frontend'
|
||||
- "./src/proxy/backend/config.yaml:/home/filebrowser/config.yaml"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "../frontend/tests/playwright-files"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
auth:
|
||||
signup: false
|
||||
methods:
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
server:
|
||||
port: 8080
|
||||
baseURL: "/"
|
||||
root: "../frontend/tests/playwright-files"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
@@ -12,9 +15,6 @@ frontend:
|
||||
auth:
|
||||
signup: false
|
||||
methods:
|
||||
password:
|
||||
enabled: false
|
||||
minLength: 0
|
||||
proxy:
|
||||
enabled: true
|
||||
header: "X-Username"
|
||||
@@ -27,9 +27,6 @@ userDefaults:
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
create: false
|
||||
rename: false
|
||||
modify: false
|
||||
delete: false
|
||||
share: false
|
||||
download: false
|
||||
api: false
|
||||
@@ -1,13 +1,19 @@
|
||||
|
||||
map $remote_addr $uuid {
|
||||
default "demo-${remote_addr}";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Username $uuid;
|
||||
add_header X-Username $uuid;
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header X-Username "proxy-user";
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,16 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "../frontend/tests/playwright-files"
|
||||
logging:
|
||||
- levels: "info|error|debug"
|
||||
sources:
|
||||
- path: "../frontend/tests/playwright-files"
|
||||
- path: "."
|
||||
name: "docker"
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
title: "Playwright"
|
||||
userDefaults:
|
||||
darkMode: true
|
||||
disableSettings: false
|
||||
scope: "."
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
create: false
|
||||
rename: false
|
||||
modify: false
|
||||
delete: false
|
||||
share: false
|
||||
download: false
|
||||
title: "Playwright"
|
||||
+47
-49
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
@@ -39,6 +40,7 @@ func (a *HookAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||
if err != nil {
|
||||
logger.Error("decode body error")
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
@@ -59,11 +61,19 @@ func (a *HookAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error
|
||||
}
|
||||
return u, nil
|
||||
case "block":
|
||||
logger.Error("block error")
|
||||
|
||||
return nil, os.ErrPermission
|
||||
case "pass":
|
||||
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
|
||||
return nil, os.ErrPermission
|
||||
logger.Error("pass error")
|
||||
|
||||
u, err := a.Users.Get(a.Cred.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get user from store: %v", err)
|
||||
}
|
||||
err = users.CheckPwd(cred.Password, u.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
default:
|
||||
@@ -138,7 +148,7 @@ func (a *HookAuth) GetValues(s string) {
|
||||
|
||||
// SaveUser updates the existing user or creates a new one when not found
|
||||
func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||
u, err := a.Users.Get(a.Cred.Username)
|
||||
if err != nil && err != errors.ErrNotExist {
|
||||
return nil, err
|
||||
}
|
||||
@@ -146,34 +156,35 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||
if u == nil {
|
||||
// create user with the provided credentials
|
||||
d := &users.User{
|
||||
Username: a.Cred.Username,
|
||||
Password: a.Cred.Password,
|
||||
Scope: a.Settings.UserDefaults.Scope,
|
||||
Locale: a.Settings.UserDefaults.Locale,
|
||||
ViewMode: a.Settings.UserDefaults.ViewMode,
|
||||
SingleClick: a.Settings.UserDefaults.SingleClick,
|
||||
Sorting: a.Settings.UserDefaults.Sorting,
|
||||
Perm: a.Settings.UserDefaults.Perm,
|
||||
Commands: a.Settings.UserDefaults.Commands,
|
||||
ShowHidden: a.Settings.UserDefaults.ShowHidden,
|
||||
NonAdminEditable: users.NonAdminEditable{
|
||||
Password: a.Cred.Password,
|
||||
Locale: a.Settings.UserDefaults.Locale,
|
||||
ViewMode: a.Settings.UserDefaults.ViewMode,
|
||||
SingleClick: a.Settings.UserDefaults.SingleClick,
|
||||
ShowHidden: a.Settings.UserDefaults.ShowHidden,
|
||||
},
|
||||
Username: a.Cred.Username,
|
||||
Perm: a.Settings.UserDefaults.Perm,
|
||||
}
|
||||
u = a.GetUser(d)
|
||||
|
||||
userHome, err := a.Settings.MakeUserDir(u.Username, u.Scope, a.Server.Root)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
|
||||
}
|
||||
u.Scope = userHome
|
||||
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", u.Username, userHome))
|
||||
files.MakeUserDirs(u)
|
||||
|
||||
err = a.Users.Save(u)
|
||||
err = a.Users.Save(u, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
|
||||
return u, nil
|
||||
}
|
||||
err = users.CheckPwd(a.Cred.Password, u.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(a.Fields.Values) > 1 {
|
||||
u = a.GetUser(u)
|
||||
// update user with provided fields
|
||||
err := a.Users.Update(u)
|
||||
err := a.Users.Update(u, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -187,29 +198,21 @@ func (a *HookAuth) GetUser(d *users.User) *users.User {
|
||||
// adds all permissions when user is admin
|
||||
isAdmin := d.Perm.Admin
|
||||
perms := users.Permissions{
|
||||
Admin: isAdmin,
|
||||
Execute: isAdmin || d.Perm.Execute,
|
||||
Create: isAdmin || d.Perm.Create,
|
||||
Rename: isAdmin || d.Perm.Rename,
|
||||
Modify: isAdmin || d.Perm.Modify,
|
||||
Delete: isAdmin || d.Perm.Delete,
|
||||
Share: isAdmin || d.Perm.Share,
|
||||
Download: isAdmin || d.Perm.Download,
|
||||
Admin: isAdmin,
|
||||
Modify: isAdmin || d.Perm.Modify,
|
||||
Share: isAdmin || d.Perm.Share,
|
||||
}
|
||||
user := users.User{
|
||||
ID: d.ID,
|
||||
Username: d.Username,
|
||||
Password: d.Password,
|
||||
Scope: d.Scope,
|
||||
Locale: d.Locale,
|
||||
ViewMode: d.ViewMode,
|
||||
SingleClick: d.SingleClick,
|
||||
Sorting: users.Sorting{
|
||||
Asc: d.Sorting.Asc,
|
||||
By: d.Sorting.By,
|
||||
NonAdminEditable: users.NonAdminEditable{
|
||||
Password: d.Password,
|
||||
Locale: a.Fields.GetString("user.locale", d.Locale),
|
||||
ViewMode: a.Fields.GetString("user.viewMode", d.ViewMode),
|
||||
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
|
||||
ShowHidden: a.Fields.GetBoolean("user.showHidden", d.ShowHidden),
|
||||
},
|
||||
Commands: d.Commands,
|
||||
ShowHidden: d.ShowHidden,
|
||||
ID: d.ID,
|
||||
Username: d.Username,
|
||||
Scopes: d.Scopes,
|
||||
Perm: perms,
|
||||
LockPassword: true,
|
||||
}
|
||||
@@ -231,16 +234,11 @@ var validHookFields = []string{
|
||||
"user.singleClick",
|
||||
"user.sorting.by",
|
||||
"user.sorting.asc",
|
||||
"user.commands",
|
||||
"user.showHidden",
|
||||
"user.perm.admin",
|
||||
"user.perm.execute",
|
||||
"user.perm.create",
|
||||
"user.perm.rename",
|
||||
"user.perm.modify",
|
||||
"user.perm.delete",
|
||||
"user.perm.share",
|
||||
"user.perm.download",
|
||||
"user.perm.api",
|
||||
}
|
||||
|
||||
// IsValid checks if the provided field is on the valid fields list
|
||||
|
||||
+11
-4
@@ -2,12 +2,13 @@ package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
@@ -27,10 +28,12 @@ func (a JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.User,
|
||||
var cred jsonCred
|
||||
|
||||
if r.Body == nil {
|
||||
logger.Error("nil body error")
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&cred)
|
||||
if err != nil {
|
||||
logger.Error("decode body error")
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
@@ -46,9 +49,13 @@ func (a JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.User,
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
}
|
||||
u, err := userStore.Get(files.RootPaths["default"], cred.Username)
|
||||
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
|
||||
return nil, os.ErrPermission
|
||||
u, err := userStore.Get(cred.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get user from store: %v", err)
|
||||
}
|
||||
err = users.CheckPwd(cred.Password, u.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
|
||||
@@ -3,7 +3,6 @@ package auth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
@@ -15,7 +14,7 @@ type NoAuth struct{}
|
||||
|
||||
// Auth uses authenticates user 1.
|
||||
func (a NoAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error) {
|
||||
return usr.Get(files.RootPaths["default"], uint(1))
|
||||
return usr.Get(uint(1))
|
||||
}
|
||||
|
||||
// LoginPage tells that no auth doesn't require a login page.
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
@@ -20,7 +19,7 @@ type ProxyAuth struct {
|
||||
// Auth authenticates the user via an HTTP header.
|
||||
func (a ProxyAuth) Auth(r *http.Request, usr *users.Storage) (*users.User, error) {
|
||||
username := r.Header.Get(a.Header)
|
||||
user, err := usr.Get(files.RootPaths["default"], username)
|
||||
user, err := usr.Get(username)
|
||||
if err == errors.ErrNotExist {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
|
||||
+76
-34
@@ -23,13 +23,57 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
)
|
||||
|
||||
func getStore(config string) (*storage.Storage, bool) {
|
||||
func getStore(configFile string) (*storage.Storage, bool) {
|
||||
// Use the config file (global flag)
|
||||
settings.Initialize(config)
|
||||
settings.Initialize(configFile)
|
||||
store, hasDB, err := storage.InitializeDb(settings.Config.Server.Database)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not load db info: %v", err))
|
||||
}
|
||||
// update source info for users if names/sources/paths might have changed
|
||||
usersList, err2 := store.Users.Gets()
|
||||
if err2 != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not load users: %v", err2))
|
||||
}
|
||||
|
||||
// this function adds scopes as needed on startup
|
||||
for _, user := range usersList {
|
||||
updateUser := false
|
||||
newScopes := []users.SourceScope{}
|
||||
for _, source := range settings.Config.Server.SourceMap {
|
||||
scopePath := ""
|
||||
if !user.Perm.Admin {
|
||||
scopePath = source.Config.DefaultUserScope
|
||||
}
|
||||
scope, err := settings.GetScopeFromSourcePath(user.Scopes, source.Path)
|
||||
if err != nil {
|
||||
if user.Perm.Admin || source.Config.DefaultEnabled {
|
||||
newScopes = append(newScopes, users.SourceScope{Scope: scopePath, Name: source.Path}) // backend name is path
|
||||
updateUser = true
|
||||
}
|
||||
} else {
|
||||
newScopes = append(newScopes, users.SourceScope{Scope: scope, Name: source.Path}) // backend name is path
|
||||
}
|
||||
}
|
||||
user.Scopes = newScopes
|
||||
// maintain backwards compatibility, update user scope from scopes
|
||||
if len(user.Scopes) == 0 {
|
||||
user.Scopes = []users.SourceScope{
|
||||
{
|
||||
Scope: user.Scope,
|
||||
Name: settings.Config.Server.DefaultSource.Path, // backend name is path
|
||||
},
|
||||
}
|
||||
updateUser = true
|
||||
}
|
||||
if !updateUser {
|
||||
continue
|
||||
}
|
||||
err := store.Users.Save(user, false)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("could not update user: %v", err))
|
||||
}
|
||||
}
|
||||
return store, hasDB
|
||||
}
|
||||
|
||||
@@ -52,9 +96,17 @@ func StartFilebrowser() {
|
||||
var help bool
|
||||
// Override the default usage output to use generalUsage()
|
||||
flag.Usage = generalUsage
|
||||
flag.StringVar(&configPath, "c", "config.yaml", "Path to the config file, default: config.yaml")
|
||||
flag.StringVar(&configPath, "c", "", "Path to the config file, default: config.yaml")
|
||||
flag.BoolVar(&help, "h", false, "Get help about commands")
|
||||
|
||||
if configPath == "" {
|
||||
configPath = os.Getenv("FILEBROWSER_CONFIG")
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
configPath = "config.yaml"
|
||||
}
|
||||
|
||||
// Parse global flags (before subcommands)
|
||||
flag.Parse() // print generalUsage on error
|
||||
|
||||
@@ -104,17 +156,23 @@ func StartFilebrowser() {
|
||||
if !ok {
|
||||
logger.Fatal("could not load db info")
|
||||
}
|
||||
user, err := store.Users.Get("", username)
|
||||
user, err := store.Users.Get(username)
|
||||
if err != nil {
|
||||
newUser := users.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
NonAdminEditable: users.NonAdminEditable{
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
if scope != "" {
|
||||
newUser.Scope = scope
|
||||
} else {
|
||||
newUser.Scope = settings.Config.UserDefaults.Scope
|
||||
for _, source := range settings.Config.Server.SourceMap {
|
||||
if source.Config.DefaultEnabled {
|
||||
newUser.Scopes = append(newUser.Scopes, users.SourceScope{
|
||||
Name: source.Name,
|
||||
Scope: source.Config.DefaultUserScope,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create the user logic
|
||||
if asAdmin {
|
||||
logger.Info(fmt.Sprintf("Creating user as admin: %s\n", username))
|
||||
@@ -128,13 +186,10 @@ func StartFilebrowser() {
|
||||
return
|
||||
}
|
||||
user.Password = password
|
||||
if scope != "" {
|
||||
user.Scope = scope
|
||||
}
|
||||
if asAdmin {
|
||||
user.Perm.Admin = true
|
||||
}
|
||||
err = store.Users.Save(user)
|
||||
err = store.Users.Save(user, true)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("could not update user: %v", err))
|
||||
}
|
||||
@@ -150,43 +205,30 @@ Release Info : https://github.com/gtsteffaniak/filebrowser/releases/tag/%v
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
store, dbExists := getStore(configPath)
|
||||
database := fmt.Sprintf("Using existing database : %v", settings.Config.Server.Database)
|
||||
if !dbExists {
|
||||
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
|
||||
}
|
||||
sources := []string{}
|
||||
for _, v := range settings.Config.Server.Sources {
|
||||
sources = append(sources, v.Name+": "+v.Path)
|
||||
sourceList := []string{}
|
||||
for path, source := range settings.Config.Server.SourceMap {
|
||||
sourceList = append(sourceList, fmt.Sprintf("%v: %v", source.Name, path))
|
||||
}
|
||||
|
||||
authMethods := []string{}
|
||||
if settings.Config.Auth.Methods.PasswordAuth.Enabled {
|
||||
authMethods = append(authMethods, "Password")
|
||||
}
|
||||
if settings.Config.Auth.Methods.ProxyAuth.Enabled {
|
||||
authMethods = append(authMethods, "Proxy")
|
||||
}
|
||||
if settings.Config.Auth.Methods.NoAuth {
|
||||
logger.Warning("Configured with no authentication, this is not recommended.")
|
||||
authMethods = []string{"Disabled"}
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Initializing FileBrowser Quantum (%v)", version.Version))
|
||||
logger.Info(fmt.Sprintf("Using Config file : %v", configPath))
|
||||
logger.Info(fmt.Sprintf("Auth Methods : %v", authMethods))
|
||||
logger.Info(fmt.Sprintf("Auth Methods : %v", settings.Config.Auth.AuthMethods))
|
||||
logger.Info(database)
|
||||
logger.Info(fmt.Sprintf("Sources : %v", sources))
|
||||
logger.Info(fmt.Sprintf("Sources : %v", sourceList))
|
||||
serverConfig := settings.Config.Server
|
||||
swagInfo := docs.SwaggerInfo
|
||||
swagInfo.BasePath = serverConfig.BaseURL
|
||||
swag.Register(docs.SwaggerInfo.InstanceName(), swagInfo)
|
||||
// initialize indexing and schedule indexing ever n minutes (default 5)
|
||||
sourceConfigs := settings.Config.Server.Sources
|
||||
if len(sourceConfigs) == 0 {
|
||||
if len(settings.Config.Server.SourceMap) == 0 {
|
||||
logger.Fatal("No sources configured, exiting...")
|
||||
}
|
||||
for _, source := range sourceConfigs {
|
||||
for _, source := range settings.Config.Server.SourceMap {
|
||||
go files.Initialize(source)
|
||||
}
|
||||
// Start the rootCMD in a goroutine
|
||||
|
||||
+3
-5
@@ -1,7 +1,8 @@
|
||||
server:
|
||||
port: 80
|
||||
baseURL: "/"
|
||||
root: "/srv"
|
||||
sources:
|
||||
- path: "/srv"
|
||||
auth:
|
||||
signup: false
|
||||
userDefaults:
|
||||
@@ -11,9 +12,6 @@ userDefaults:
|
||||
singleClick: false
|
||||
permissions:
|
||||
admin: false
|
||||
create: false
|
||||
rename: false
|
||||
modify: false
|
||||
delete: false
|
||||
share: false
|
||||
download: false
|
||||
api: false
|
||||
@@ -1,2 +0,0 @@
|
||||
server:
|
||||
root: "/srv"
|
||||
+48
-18
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
)
|
||||
|
||||
@@ -73,7 +72,6 @@ type FileOptions struct {
|
||||
Modify bool
|
||||
Expand bool
|
||||
ReadHeader bool
|
||||
Checker users.Checker
|
||||
Content bool
|
||||
}
|
||||
|
||||
@@ -84,7 +82,7 @@ func (f FileOptions) Components() (string, string) {
|
||||
func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
|
||||
response := ExtendedFileInfo{}
|
||||
if opts.Source == "" {
|
||||
opts.Source = "default"
|
||||
opts.Source = settings.Config.Server.DefaultSource.Name
|
||||
}
|
||||
index := GetIndex(opts.Source)
|
||||
if index == nil {
|
||||
@@ -95,9 +93,6 @@ func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
|
||||
pathMutex := getMutex(opts.Path)
|
||||
pathMutex.Lock()
|
||||
defer pathMutex.Unlock()
|
||||
if !opts.Checker.Check(opts.Path) {
|
||||
return response, os.ErrPermission
|
||||
}
|
||||
|
||||
realPath, isDir, err := index.GetRealPath(opts.Path)
|
||||
if err != nil {
|
||||
@@ -133,7 +128,7 @@ func FileInfoFaster(opts FileOptions) (ExtendedFileInfo, error) {
|
||||
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
|
||||
}
|
||||
if opts.Content {
|
||||
content, err := getContent("default", opts.Path)
|
||||
content, err := getContent(realPath)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
@@ -198,6 +193,9 @@ func DeleteFiles(source, absPath string, dirPath string) error {
|
||||
return err
|
||||
}
|
||||
index := GetIndex(source)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", source)
|
||||
}
|
||||
refreshConfig := FileOptions{Path: dirPath, IsDir: true}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
@@ -206,12 +204,16 @@ func DeleteFiles(source, absPath string, dirPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||
func MoveResource(sourceIndex, dstIndex, realsrc, realdst string, isSrcDir bool) error {
|
||||
err := fileutils.MoveFile(realsrc, realdst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := GetIndex(source)
|
||||
|
||||
index := GetIndex(sourceIndex)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: realsrc,
|
||||
@@ -224,6 +226,10 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||
if !isSrcDir {
|
||||
refreshConfig.Path = filepath.Dir(realdst)
|
||||
}
|
||||
index = GetIndex(dstIndex)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", dstIndex)
|
||||
}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not refresh index for dest: %v", err)
|
||||
@@ -231,25 +237,48 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyResource(source, realsrc, realdst string, isSrcDir bool) error {
|
||||
func CopyResource(sourceIndex, dstIndex, realsrc, realdst string, isSrcDir bool) error {
|
||||
err := fileutils.CopyFile(realsrc, realdst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index := GetIndex(source)
|
||||
index := GetIndex(sourceIndex)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", sourceIndex)
|
||||
}
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: realsrc,
|
||||
IsDir: isSrcDir,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not refresh index for source: %v", err)
|
||||
}
|
||||
refreshConfig := FileOptions{Path: realdst, IsDir: true}
|
||||
if !isSrcDir {
|
||||
refreshConfig.Path = filepath.Dir(realdst)
|
||||
}
|
||||
index = GetIndex(dstIndex)
|
||||
if index == nil {
|
||||
return fmt.Errorf("could not get index: %v ", dstIndex)
|
||||
}
|
||||
refreshConfig = FileOptions{Path: realdst, IsDir: true}
|
||||
if !isSrcDir {
|
||||
refreshConfig.Path = filepath.Dir(realdst)
|
||||
}
|
||||
err = index.RefreshFileInfo(refreshConfig)
|
||||
if err != nil {
|
||||
return errors.ErrEmptyKey
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteDirectory(opts FileOptions) error {
|
||||
idx := GetIndex(opts.Source)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
realPath, _, _ := idx.GetRealPath(opts.Path)
|
||||
// Ensure the parent directories exist
|
||||
err := os.MkdirAll(realPath, 0775)
|
||||
@@ -265,6 +294,9 @@ func WriteDirectory(opts FileOptions) error {
|
||||
|
||||
func WriteFile(opts FileOptions, in io.Reader) error {
|
||||
idx := GetIndex(opts.Source)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
dst, _, _ := idx.GetRealPath(opts.Path)
|
||||
parentDir := filepath.Dir(dst)
|
||||
// Create the directory and all necessary parents
|
||||
@@ -319,13 +351,7 @@ func resolveSymlinks(path string) (string, bool, error) {
|
||||
}
|
||||
|
||||
// addContent reads and sets content based on the file type.
|
||||
func getContent(source, path string) (string, error) {
|
||||
idx := GetIndex(source)
|
||||
realPath, _, err := idx.GetRealPath(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
func getContent(realPath string) (string, error) {
|
||||
content, err := os.ReadFile(realPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -377,6 +403,10 @@ func (i *ExtendedFileInfo) detectSubtitles(path string) {
|
||||
}
|
||||
|
||||
idx := GetIndex(i.Source)
|
||||
if idx == nil {
|
||||
return
|
||||
}
|
||||
|
||||
parentInfo, exists := idx.GetReducedMetadata(filepath.Dir(i.Path), true)
|
||||
if !exists {
|
||||
return
|
||||
|
||||
@@ -35,23 +35,20 @@ type Index struct {
|
||||
var (
|
||||
indexes map[string]*Index
|
||||
indexesMutex sync.RWMutex
|
||||
RootPaths map[string]string
|
||||
)
|
||||
|
||||
func init() {
|
||||
indexes = make(map[string]*Index)
|
||||
}
|
||||
|
||||
func Initialize(source settings.Source) {
|
||||
indexesMutex.RLock()
|
||||
indexesMutex.Lock()
|
||||
newIndex := Index{
|
||||
Source: source,
|
||||
Directories: make(map[string]*FileInfo),
|
||||
}
|
||||
if RootPaths == nil {
|
||||
RootPaths = make(map[string]string)
|
||||
}
|
||||
RootPaths[source.Name] = source.Path
|
||||
indexes = make(map[string]*Index)
|
||||
indexes[newIndex.Source.Name] = &newIndex
|
||||
indexesMutex.RUnlock()
|
||||
|
||||
indexesMutex.Unlock()
|
||||
if !newIndex.Source.Config.Disabled {
|
||||
time.Sleep(time.Second)
|
||||
logger.Info(fmt.Sprintf("initializing index: [%v]", newIndex.Source.Name))
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
var scanSchedule = []time.Duration{
|
||||
5 * time.Minute, // 5 minute quick scan & 25 minutes for a full scan
|
||||
10 * time.Minute,
|
||||
20 * time.Minute, // [3] element is 20 minutes, reset anchor for full scan
|
||||
40 * time.Minute,
|
||||
20 * time.Minute,
|
||||
40 * time.Minute, // [3] element is 40 minutes, reset anchor for full scan
|
||||
1 * time.Hour,
|
||||
2 * time.Hour,
|
||||
3 * time.Hour,
|
||||
@@ -48,6 +48,7 @@ func (idx *Index) newScanner(origin string) {
|
||||
|
||||
// Adjust schedule based on file changes
|
||||
if idx.FilesChangedDuringIndexing {
|
||||
logger.Debug(fmt.Sprintf("Files changed during indexing [%v], adjusting schedule.", idx.Name))
|
||||
// Move to at least the full-scan anchor or reduce interval
|
||||
if idx.currentSchedule > fullScanAnchor {
|
||||
idx.currentSchedule = fullScanAnchor
|
||||
@@ -106,9 +107,9 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
|
||||
idx.assessment = "normal"
|
||||
}
|
||||
if firstRun {
|
||||
logger.Info(fmt.Sprintf("Index assessment : index=%v complexity=%v directories=%v files=%v", idx.Source.Name, idx.assessment, idx.NumDirs, idx.NumFiles))
|
||||
logger.Info(fmt.Sprintf("Index assessment : [%v] complexity=%v directories=%v files=%v", idx.Source.Name, idx.assessment, idx.NumDirs, idx.NumFiles))
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("Index assessment : iindex=%v complexity=%v directories=%v files=%v", idx.Source.Name, idx.assessment, idx.NumDirs, idx.NumFiles))
|
||||
logger.Debug(fmt.Sprintf("Index assessment : [%v] complexity=%v directories=%v files=%v", idx.Source.Name, idx.assessment, idx.NumDirs, idx.NumFiles))
|
||||
}
|
||||
if idx.NumDirs != prevNumDirs || idx.NumFiles != prevNumFiles {
|
||||
idx.FilesChangedDuringIndexing = true
|
||||
|
||||
@@ -16,6 +16,9 @@ func BenchmarkFillIndex(b *testing.B) {
|
||||
Path: "/srv",
|
||||
})
|
||||
idx := GetIndex("test")
|
||||
if idx == nil {
|
||||
b.Fatal("index is nil")
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -165,7 +168,7 @@ func BenchmarkCheckIndexExclude(b *testing.B) {
|
||||
idx := Index{
|
||||
Source: settings.Source{
|
||||
Name: "files",
|
||||
Config: settings.IndexConfig{
|
||||
Config: settings.SourceConfig{
|
||||
IgnoreHidden: true,
|
||||
Exclude: settings.IndexFilter{
|
||||
Files: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
|
||||
@@ -202,7 +205,7 @@ func BenchmarkCheckIndexConditionsInclude(b *testing.B) {
|
||||
idx2 := Index{
|
||||
Source: settings.Source{
|
||||
Name: "files",
|
||||
Config: settings.IndexConfig{
|
||||
Config: settings.SourceConfig{
|
||||
IgnoreHidden: true,
|
||||
Include: settings.IndexFilter{
|
||||
Files: []string{"test", "filepath", ".test", ".filepath", "test", "filepath", ".test", ".filepath"},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
|
||||
@@ -71,9 +72,7 @@ func GetIndex(name string) *Index {
|
||||
defer indexesMutex.Unlock()
|
||||
index, ok := indexes[name]
|
||||
if !ok {
|
||||
return &Index{
|
||||
Source: settings.Source{Name: "default", Path: "."},
|
||||
}
|
||||
logger.Error(fmt.Sprintf("index %s not found", name))
|
||||
}
|
||||
return index
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package settings
|
||||
package files
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
@@ -10,6 +9,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -17,25 +18,39 @@ var (
|
||||
dashes = regexp.MustCompile(`[\-]+`)
|
||||
)
|
||||
|
||||
// todo fix this!!
|
||||
// MakeUserDir makes the user directory according to settings.
|
||||
func (s *Settings) MakeUserDir(username, userScope, serverRoot string) (string, error) {
|
||||
userScope = strings.TrimSpace(userScope)
|
||||
if userScope == "" && s.Server.CreateUserDir {
|
||||
func (idx *Index) MakeUserDir(username string, scope string) error {
|
||||
if idx.Config.CreateUserDir {
|
||||
username = cleanUsername(username)
|
||||
if username == "" || username == "-" || username == "." {
|
||||
logger.Error(fmt.Sprintf("create user: invalid user for home dir creation: [%s]", username))
|
||||
return "", errors.New("invalid user for home dir creation")
|
||||
}
|
||||
userScope = path.Join(s.Server.UserHomeBasePath, username)
|
||||
}
|
||||
|
||||
userScope = path.Join("/", userScope)
|
||||
|
||||
fullPath := filepath.Join(serverRoot, userScope)
|
||||
userScope := path.Join("/", scope)
|
||||
fullPath := filepath.Join(idx.Path, userScope)
|
||||
logger.Debug(fmt.Sprintf("creating user home dir: [%s]", fullPath))
|
||||
if err := os.MkdirAll(fullPath, os.ModePerm); err != nil {
|
||||
return "", fmt.Errorf("failed to create user home dir: [%s]: %w", userScope, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MakeUserDirs(u *users.User) {
|
||||
for _, scope := range u.Scopes {
|
||||
source := settings.Config.Server.NameToSource[scope.Name]
|
||||
if source.Config.CreateUserDir {
|
||||
idx := GetIndex(source.Name)
|
||||
if idx == nil {
|
||||
logger.Error(fmt.Sprintf("create user: failed to find source index for user home dir creation: %s", source.Name))
|
||||
continue
|
||||
}
|
||||
err := idx.MakeUserDir(u.Username, scope.Scope)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("create user: failed to create user home dir: %s", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
return userScope, nil
|
||||
}
|
||||
|
||||
func cleanUsername(s string) string {
|
||||
@@ -1,28 +1,43 @@
|
||||
package fileutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
// MoveFile moves a file from src to dst.
|
||||
// By default, the rename system call is used. If src and dst point to different volumes,
|
||||
// the file copy is used as a fallback.
|
||||
func MoveFile(src, dst string) error {
|
||||
if os.Rename(src, dst) == nil {
|
||||
err := os.Rename(src, dst)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fallback
|
||||
err := CopyFile(src, dst)
|
||||
err = CopyFile(src, dst)
|
||||
if err != nil {
|
||||
_ = os.Remove(dst)
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(src); err != nil {
|
||||
go func() {
|
||||
err = os.Remove(src)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("os.Remove failed %v %v ", src, err))
|
||||
}
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = os.Remove(src)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("os.Remove failed %v %v ", src, err))
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -8,7 +8,6 @@ require (
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
|
||||
github.com/goccy/go-yaml v1.15.23
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1
|
||||
github.com/google/go-cmp v0.6.0
|
||||
@@ -20,7 +19,6 @@ require (
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/image v0.24.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/text v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -44,6 +42,7 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -27,8 +27,6 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
|
||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
|
||||
+55
-14
@@ -10,31 +10,45 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// createApiKeyHandler creates an API key for the user.
|
||||
// @Summary Create API key
|
||||
// @Description Create an API key with specified name, duration, and permissions.
|
||||
// @Tags API Keys
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name query string true "Name of the API key"
|
||||
// @Param days query string true "Duration of the API key in days"
|
||||
// @Param permissions query string true "Permissions for the API key (comma-separated)"
|
||||
// @Success 200 {object} HttpResponse "Token created successfully, resoponse contains json object with token key"
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Failure 404 {object} map[string]string "Not found"
|
||||
// @Failure 409 {object} map[string]string "Conflict"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/createApiKey [post]
|
||||
func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
name := r.URL.Query().Get("name")
|
||||
durationStr := r.URL.Query().Get("days")
|
||||
permissionsStr := r.URL.Query().Get("permissions")
|
||||
|
||||
if !d.user.Perm.Api {
|
||||
return http.StatusForbidden, fmt.Errorf("user does not have permission to create api keys")
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return http.StatusInternalServerError, fmt.Errorf("api name must be valid")
|
||||
return http.StatusBadRequest, fmt.Errorf("api name must be valid")
|
||||
}
|
||||
if durationStr == "" {
|
||||
return http.StatusInternalServerError, fmt.Errorf("api duration must be valid")
|
||||
return http.StatusBadRequest, fmt.Errorf("api duration must be valid")
|
||||
}
|
||||
if permissionsStr == "" {
|
||||
return http.StatusInternalServerError, fmt.Errorf("api permissions must be valid")
|
||||
return http.StatusBadRequest, fmt.Errorf("api permissions must be valid")
|
||||
}
|
||||
// Parse permissions from the query parameter
|
||||
permissions := users.Permissions{
|
||||
Api: strings.Contains(permissionsStr, "api") && d.user.Perm.Api,
|
||||
Admin: strings.Contains(permissionsStr, "admin") && d.user.Perm.Admin,
|
||||
Execute: strings.Contains(permissionsStr, "execute") && d.user.Perm.Execute,
|
||||
Create: strings.Contains(permissionsStr, "create") && d.user.Perm.Create,
|
||||
Rename: strings.Contains(permissionsStr, "rename") && d.user.Perm.Rename,
|
||||
Modify: strings.Contains(permissionsStr, "modify") && d.user.Perm.Modify,
|
||||
Delete: strings.Contains(permissionsStr, "delete") && d.user.Perm.Delete,
|
||||
Share: strings.Contains(permissionsStr, "share") && d.user.Perm.Share,
|
||||
Download: strings.Contains(permissionsStr, "download") && d.user.Perm.Download,
|
||||
Api: strings.Contains(permissionsStr, "api") && d.user.Perm.Api,
|
||||
Admin: strings.Contains(permissionsStr, "admin") && d.user.Perm.Admin,
|
||||
Modify: strings.Contains(permissionsStr, "modify") && d.user.Perm.Modify,
|
||||
Share: strings.Contains(permissionsStr, "share") && d.user.Perm.Share,
|
||||
}
|
||||
|
||||
// Convert the duration string to an int64
|
||||
@@ -45,7 +59,6 @@ func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
|
||||
// Here we assume the duration is in seconds; convert to time.Duration
|
||||
duration := time.Duration(durationInt) * time.Hour * 24
|
||||
|
||||
// get request body like:
|
||||
token, err := makeSignedTokenAPI(d.user, name, duration, permissions)
|
||||
if err != nil {
|
||||
@@ -61,8 +74,22 @@ func createApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
return renderJSON(w, r, response)
|
||||
}
|
||||
|
||||
// deleteApiKeyHandler deletes an API key for the user.
|
||||
// @Summary Delete API key
|
||||
// @Description Delete an API key with specified name.
|
||||
// @Tags API Keys
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name query string true "Name of the API key to delete"
|
||||
// @Success 200 {object} HttpResponse "API key deleted successfully"
|
||||
// @Failure 404 {object} map[string]string "Not found"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/deleteApiKey [delete]
|
||||
func deleteApiKeyHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
name := r.URL.Query().Get("name")
|
||||
if !d.user.Perm.Api {
|
||||
return http.StatusForbidden, fmt.Errorf("user does not have permission to delete api keys")
|
||||
}
|
||||
|
||||
keyInfo, ok := d.user.ApiKeys[name]
|
||||
if !ok {
|
||||
@@ -89,8 +116,22 @@ type AuthTokenMin struct {
|
||||
Permissions users.Permissions `json:"Permissions"`
|
||||
}
|
||||
|
||||
// listApiKeysHandler lists all API keys or retrieves details for a specific key.
|
||||
// @Summary List API keys
|
||||
// @Description List all API keys or retrieve details for a specific key.
|
||||
// @Tags API Keys
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name query string false "Name of the API to retrieve details"
|
||||
// @Success 200 {object} AuthTokenMin "List of API keys or specific key details"
|
||||
// @Failure 404 {object} map[string]string "Not found"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/listApiKeys [get]
|
||||
func listApiKeysHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
key := r.URL.Query().Get("key")
|
||||
key := r.URL.Query().Get("name")
|
||||
if !d.user.Perm.Api {
|
||||
return http.StatusForbidden, fmt.Errorf("user does not have permission to list api keys")
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
keyInfo, ok := d.user.ApiKeys[key]
|
||||
|
||||
+39
-17
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -17,7 +16,6 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
@@ -74,6 +72,16 @@ func extractToken(r *http.Request) (string, error) {
|
||||
return "", request.ErrNoTokenInRequest
|
||||
}
|
||||
|
||||
// loginHandler handles user authentication via password.
|
||||
// @Summary User login
|
||||
// @Description Authenticate a user with a username and password.
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "JWT token for authentication"
|
||||
// @Failure 403 {object} map[string]string "Forbidden - authentication failed"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/login [post]
|
||||
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !config.Auth.Methods.PasswordAuth.Enabled {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
@@ -88,12 +96,9 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// Authenticate the user based on the request
|
||||
user, err := auther.Auth(r, store.Users)
|
||||
if err == os.ErrPermission {
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
status, err := printToken(w, r, user) // Pass the data object
|
||||
if err != nil {
|
||||
@@ -106,6 +111,19 @@ type signupBody struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// signupHandler registers a new user account.
|
||||
// @Summary User signup
|
||||
// @Description Register a new user account with a username and password.
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body signupBody true "User signup details"
|
||||
// @Success 201 {string} string "User created successfully"
|
||||
// @Failure 400 {object} map[string]string "Bad request - invalid input"
|
||||
// @Failure 405 {object} map[string]string "Method not allowed - signup is disabled"
|
||||
// @Failure 409 {object} map[string]string "Conflict - user already exists"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/signup [post]
|
||||
func signupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !settings.Config.Auth.Signup {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
@@ -128,20 +146,14 @@ func signupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user := settings.ApplyUserDefaults(users.User{})
|
||||
user := &users.User{}
|
||||
settings.ApplyUserDefaults(user)
|
||||
user.Username = info.Username
|
||||
user.Password = info.Password
|
||||
|
||||
userHome, err := config.MakeUserDir(user.Username, user.Scope, files.RootPaths["default"])
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("create user: failed to mkdir user home dir: [%s]", userHome))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user.Scope = userHome
|
||||
logger.Debug(fmt.Sprintf("new user: %s, home dir: [%s].", user.Username, userHome))
|
||||
err = store.Users.Save(&user)
|
||||
files.MakeUserDirs(user)
|
||||
|
||||
err = store.Users.Save(user, true)
|
||||
if err == errors.ErrExist {
|
||||
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
|
||||
return
|
||||
@@ -151,6 +163,16 @@ func signupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// renewHandler refreshes the authentication token for a logged-in user.
|
||||
// @Summary Renew authentication token
|
||||
// @Description Refresh the authentication token for a logged-in user.
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "New JWT token generated"
|
||||
// @Failure 401 {object} map[string]string "Unauthorized - invalid token"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/renew [post]
|
||||
func renewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// check if x-auth header is present and token is
|
||||
return printToken(w, r, d.user)
|
||||
|
||||
+18
-27
@@ -5,23 +5,23 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/runner"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
type requestContext struct {
|
||||
user *users.User
|
||||
*runner.Runner
|
||||
user *users.User
|
||||
raw interface{}
|
||||
path string
|
||||
token string
|
||||
share *share.Link
|
||||
}
|
||||
|
||||
type HttpResponse struct {
|
||||
@@ -42,13 +42,12 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
hash := r.URL.Query().Get("hash")
|
||||
// Retrieve the user (using the public user by default)
|
||||
data.user = &users.PublicUser
|
||||
data.user.Scope = ""
|
||||
|
||||
// Get the file link by hash
|
||||
link, err := store.Share.GetByHash(hash)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, fmt.Errorf("share not found")
|
||||
}
|
||||
data.share = link
|
||||
// Authenticate the share request if needed
|
||||
var status int
|
||||
if link.Hash != "" {
|
||||
@@ -57,16 +56,13 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
return status, fmt.Errorf("could not authenticate share request")
|
||||
}
|
||||
}
|
||||
|
||||
// Set the scope to configured defaults
|
||||
scope := settings.Config.UserDefaults.Scope
|
||||
|
||||
data.path = link.Path + "/" + path
|
||||
// Get file information with options
|
||||
file, err := FileInfoFasterFunc(files.FileOptions{
|
||||
Path: filepath.Join(scope, link.Path+"/"+path),
|
||||
Modify: data.user.Perm.Modify,
|
||||
Expand: true,
|
||||
Checker: data.user, // Call your checker function here
|
||||
Path: data.path,
|
||||
Source: link.Source,
|
||||
Modify: false,
|
||||
Expand: true,
|
||||
})
|
||||
file.Token = link.Token
|
||||
if err != nil {
|
||||
@@ -88,8 +84,6 @@ func withAdminHelper(fn handleFunc) handleFunc {
|
||||
if !data.user.Perm.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Proceed to the actual handler if the user is admin
|
||||
return fn(w, r, data)
|
||||
})
|
||||
}
|
||||
@@ -100,7 +94,7 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
if config.Auth.Methods.NoAuth {
|
||||
var err error
|
||||
// Retrieve the user from the store and store it in the context
|
||||
data.user, err = store.Users.Get(files.RootPaths["default"], uint(1))
|
||||
data.user, err = store.Users.Get(uint(1))
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("no auth: %v", err))
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -111,20 +105,19 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
|
||||
var err error
|
||||
// Retrieve the user from the store and store it in the context
|
||||
data.user, err = store.Users.Get(files.RootPaths["default"], proxyUser)
|
||||
data.user, err = store.Users.Get(proxyUser)
|
||||
if err != nil {
|
||||
if err.Error() != "the resource does not exist" {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if config.Auth.Methods.ProxyAuth.CreateUser {
|
||||
newUser := settings.ApplyUserDefaults(users.User{
|
||||
Username: proxyUser,
|
||||
})
|
||||
err := store.Users.Save(&newUser)
|
||||
newUser := &users.User{Username: proxyUser}
|
||||
settings.ApplyUserDefaults(newUser)
|
||||
err := store.Users.Save(newUser, false)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
data.user, err = store.Users.Get(files.RootPaths["default"], proxyUser)
|
||||
data.user, err = store.Users.Get(proxyUser)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -161,7 +154,7 @@ func withUserHelper(fn handleFunc) handleFunc {
|
||||
}
|
||||
|
||||
// Retrieve the user from the store and store it in the context
|
||||
data.user, err = store.Users.Get(files.RootPaths["default"], tk.BelongsTo)
|
||||
data.user, err = store.Users.Get(tk.BelongsTo)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -187,9 +180,7 @@ func withSelfOrAdminHelper(fn handleFunc) handleFunc {
|
||||
|
||||
func wrapHandler(fn handleFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data := &requestContext{
|
||||
Runner: &runner.Runner{Enabled: config.Server.EnableExec, Settings: config},
|
||||
}
|
||||
data := &requestContext{}
|
||||
|
||||
// Call the actual handler function and get status code and error
|
||||
status, err := fn(w, r, data)
|
||||
|
||||
@@ -75,10 +75,10 @@ func TestWithAdminHelper(t *testing.T) {
|
||||
Perm: users.Permissions{Admin: false}, // Non-admin user
|
||||
}
|
||||
// Save the users to the mock database
|
||||
if err := store.Users.Save(adminUser); err != nil {
|
||||
if err := store.Users.Save(adminUser, true); err != nil {
|
||||
t.Fatal("failed to save admin user:", err)
|
||||
}
|
||||
if err := store.Users.Save(nonAdminUser); err != nil {
|
||||
if err := store.Users.Save(nonAdminUser, true); err != nil {
|
||||
t.Fatal("failed to save non-admin user:", err)
|
||||
}
|
||||
// Test cases for different scenarios
|
||||
|
||||
+27
-23
@@ -14,6 +14,7 @@ import (
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/cache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
@@ -43,7 +44,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
encodedUrl := r.URL.Query().Get("url")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
source = settings.Config.Server.DefaultSource.Name
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
url, err := url.QueryUnescape(encodedUrl)
|
||||
@@ -60,12 +61,15 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
replacement := strings.Split(url, "/api/raw")[0]
|
||||
url = strings.Replace(url, replacement, settings.Config.Server.InternalUrl, 1)
|
||||
}
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
Checker: d.user,
|
||||
Path: filepath.Join(userscope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -92,8 +96,8 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
"url": url + "&auth=" + d.token,
|
||||
"permissions": map[string]interface{}{
|
||||
"edit": d.user.Perm.Modify,
|
||||
"download": d.user.Perm.Download,
|
||||
"print": d.user.Perm.Download,
|
||||
"download": true,
|
||||
"print": true,
|
||||
},
|
||||
},
|
||||
"editorConfig": map[string]interface{}{
|
||||
@@ -137,7 +141,7 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
source = settings.Config.Server.DefaultSource.Name
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
@@ -167,20 +171,13 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
}
|
||||
defer doc.Body.Close()
|
||||
|
||||
err = d.Runner.RunHook(func() error {
|
||||
fileOpts := files.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
}
|
||||
writeErr := files.WriteFile(fileOpts, doc.Body)
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
return nil
|
||||
}, "save", path, "", d.user)
|
||||
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
fileOpts := files.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
}
|
||||
writeErr := files.WriteFile(fileOpts, doc.Body)
|
||||
if writeErr != nil {
|
||||
return http.StatusInternalServerError, writeErr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +189,9 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
|
||||
func getOnlyOfficeId(source, path string) (string, error) {
|
||||
idx := files.GetIndex(source)
|
||||
if idx == nil {
|
||||
return "", fmt.Errorf("source not found")
|
||||
}
|
||||
realpath, _, _ := idx.GetRealPath(path)
|
||||
// error is intentionally ignored in order treat errors
|
||||
// the same as a cache-miss
|
||||
@@ -203,6 +203,10 @@ func getOnlyOfficeId(source, path string) (string, error) {
|
||||
}
|
||||
func deleteOfficeId(source, path string) {
|
||||
idx := files.GetIndex(source)
|
||||
if idx == nil {
|
||||
logger.Error(fmt.Sprintf("deleteOfficeId: failed to find source index for user home dir creation: %s", source))
|
||||
return
|
||||
}
|
||||
realpath, _, _ := idx.GetRealPath(path)
|
||||
cache.OnlyOffice.Delete(realpath)
|
||||
}
|
||||
+21
-18
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/img"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
type ImgService interface {
|
||||
@@ -45,6 +46,9 @@ type FileCache interface {
|
||||
func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = settings.Config.Server.DefaultSource.Name
|
||||
}
|
||||
previewSize := r.URL.Query().Get("size")
|
||||
if previewSize != "small" {
|
||||
previewSize = "large"
|
||||
@@ -52,14 +56,16 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
if path == "" {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid request path")
|
||||
}
|
||||
response, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Checker: d.user,
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(userscope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
})
|
||||
fileInfo := response.FileInfo
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -73,37 +79,31 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
|
||||
if (previewSize == "large" && !config.Server.ResizePreview) ||
|
||||
(previewSize == "small" && !config.Server.EnableThumbnails) {
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
return rawFileHandler(w, r, fileInfo)
|
||||
}
|
||||
|
||||
format, err := imgSvc.FormatFromExtension(filepath.Ext(fileInfo.Name))
|
||||
// Unsupported extensions directly return the raw data
|
||||
if err == img.ErrUnsupportedFormat || format == img.FormatGif {
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
return rawFileHandler(w, r, fileInfo)
|
||||
}
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
cacheKey := previewCacheKey(response.RealPath, previewSize, fileInfo.ModTime)
|
||||
cacheKey := previewCacheKey(fileInfo.RealPath, previewSize, fileInfo.ModTime)
|
||||
resizedImage, ok, err := fileCache.Load(r.Context(), cacheKey)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
resizedImage, err = createPreview(imgSvc, fileCache, response, previewSize)
|
||||
resizedImage, err = createPreview(imgSvc, fileCache, fileInfo, previewSize)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
}
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
http.ServeContent(w, r, response.RealPath, fileInfo.ModTime, bytes.NewReader(resizedImage))
|
||||
http.ServeContent(w, r, fileInfo.RealPath, fileInfo.ModTime, bytes.NewReader(resizedImage))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -153,8 +153,11 @@ func previewCacheKey(realPath, previewSize string, modTime time.Time) string {
|
||||
return fmt.Sprintf("%x%x%x", realPath, modTime.Unix(), previewSize)
|
||||
}
|
||||
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file files.FileInfo) (int, error) {
|
||||
idx := files.GetIndex("default")
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file files.ExtendedFileInfo) (int, error) {
|
||||
idx := files.GetIndex(file.Source)
|
||||
if idx == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not found: %s", file.Source)
|
||||
}
|
||||
realPath, _, _ := idx.GetRealPath(file.Path)
|
||||
fd, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
|
||||
+36
-1
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
@@ -12,12 +13,46 @@ import (
|
||||
_ "github.com/gtsteffaniak/filebrowser/backend/swagger/docs"
|
||||
)
|
||||
|
||||
// rawHandler serves the raw content of a file, multiple files, or directory in various formats.
|
||||
// @Summary Get raw content of a file, multiple files, or directory
|
||||
// @Description Returns the raw content of a file, multiple files, or a directory. Supports downloading files as archives in various formats.
|
||||
// @Tags Resources
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param files query string true "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)"
|
||||
// @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"
|
||||
// @Failure 202 {object} map[string]string "Modify permissions required"
|
||||
// @Failure 400 {object} map[string]string "Invalid request path"
|
||||
// @Failure 404 {object} map[string]string "File or directory not found"
|
||||
// @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) {
|
||||
filePrefix := ""
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if ok {
|
||||
filePrefix = file.Source + "::"
|
||||
}
|
||||
encodedFiles := file.Path + r.URL.Query().Get("files")
|
||||
// Decode the URL-encoded path
|
||||
files, err := url.QueryUnescape(encodedFiles)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
fileList := strings.Split(files, "||")
|
||||
for i, f := range fileList {
|
||||
fileList[i] = filePrefix + f
|
||||
}
|
||||
return rawFilesHandler(w, r, d, fileList)
|
||||
}
|
||||
|
||||
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
file.Path = strings.TrimPrefix(file.Path, files.RootPaths["default"])
|
||||
file.Path = strings.TrimPrefix(file.Path, file.Source)
|
||||
return renderJSON(w, r, file)
|
||||
}
|
||||
|
||||
|
||||
+43
-16
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName string) {
|
||||
@@ -32,20 +33,16 @@ func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName stri
|
||||
// @Tags Resources
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param files query string true "Comma-separated list of specific files within the directory (required)"
|
||||
// @Param files query string true "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)"
|
||||
// @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"
|
||||
// @Failure 202 {object} map[string]string "Download permissions required"
|
||||
// @Failure 202 {object} map[string]string "Modify permissions required"
|
||||
// @Failure 400 {object} map[string]string "Invalid request path"
|
||||
// @Failure 404 {object} map[string]string "File or directory not found"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/raw [get]
|
||||
func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
|
||||
filePrefix := ""
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if ok {
|
||||
@@ -57,7 +54,7 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
fileList := strings.Split(files, ",|")
|
||||
fileList := strings.Split(files, "||")
|
||||
for i, f := range fileList {
|
||||
fileList[i] = filepath.Join(filePrefix, f)
|
||||
}
|
||||
@@ -65,10 +62,25 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
||||
}
|
||||
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer, flatten bool) error {
|
||||
idx := files.GetIndex("default")
|
||||
realPath, _, _ := idx.GetRealPath(d.user.Scope, path)
|
||||
if !d.user.Check(realPath) {
|
||||
return nil
|
||||
splitFile := strings.Split(path, "::")
|
||||
if len(splitFile) != 2 {
|
||||
return fmt.Errorf("invalid file in files requested: %v", splitFile)
|
||||
}
|
||||
source := splitFile[0]
|
||||
path = splitFile[1]
|
||||
userScope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if d.share == nil && err != nil {
|
||||
return fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
|
||||
}
|
||||
|
||||
idx := files.GetIndex(source)
|
||||
if idx == nil {
|
||||
return fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
|
||||
realPath, _, err := idx.GetRealPath(userScope, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get real path for %s: %v", path, err)
|
||||
}
|
||||
info, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
@@ -173,10 +185,24 @@ func addSingleFile(realPath, archivePath string, zipWriter *zip.Writer, tarWrite
|
||||
}
|
||||
|
||||
func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext, fileList []string) (int, error) {
|
||||
filePath := fileList[0]
|
||||
fileName := filepath.Base(filePath)
|
||||
idx := files.GetIndex("default")
|
||||
realPath, isDir, err := idx.GetRealPath(d.user.Scope, filePath)
|
||||
splitFile := strings.Split(fileList[0], "::")
|
||||
if len(splitFile) != 2 {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file in files request: %v", fileList[0])
|
||||
}
|
||||
|
||||
firstFileSource := splitFile[0]
|
||||
firstFilePath := splitFile[1]
|
||||
fileName := filepath.Base(firstFilePath)
|
||||
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, firstFileSource)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
idx := files.GetIndex(firstFileSource)
|
||||
if idx == nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", firstFileSource)
|
||||
}
|
||||
realPath, isDir, err := idx.GetRealPath(userscope, firstFilePath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
@@ -246,9 +272,9 @@ func createZip(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
err := addFile(fname, d, nil, zipWriter, false)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to ZIP: %v", fname, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -265,6 +291,7 @@ func createTarGz(w io.Writer, d *requestContext, filenames ...string) error {
|
||||
err := addFile(fname, d, tarWriter, nil, false)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("Failed to add %s to TAR.GZ: %v", fname, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+113
-61
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// resourceGetHandler retrieves information about a resource.
|
||||
@@ -38,19 +39,22 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
source = config.Server.DefaultSource.Name
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Path: filepath.Join(userscope, path),
|
||||
Modify: d.user.Perm.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Checker: d.user,
|
||||
Content: r.URL.Query().Get("content") == "true",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -61,7 +65,10 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
}
|
||||
if algo := r.URL.Query().Get("checksum"); algo != "" {
|
||||
idx := files.GetIndex(source)
|
||||
realPath, _, _ := idx.GetRealPath(d.user.Scope, path)
|
||||
if idx == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source %s not found", source)
|
||||
}
|
||||
realPath, _, _ := idx.GetRealPath(userscope, path)
|
||||
checksums, err := files.GetChecksum(realPath, algo)
|
||||
if err == errors.ErrInvalidOption {
|
||||
return http.StatusBadRequest, nil
|
||||
@@ -93,22 +100,25 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
source = config.Server.DefaultSource.Name
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
if path == "/" || !d.user.Perm.Delete {
|
||||
if path == "/" {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Checker: d.user,
|
||||
Path: filepath.Join(userscope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(fileOpts)
|
||||
if err != nil {
|
||||
@@ -146,22 +156,27 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
source = config.Server.DefaultSource.Name
|
||||
}
|
||||
|
||||
if !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
if !d.user.Perm.Create || !d.user.Check(path) {
|
||||
return http.StatusForbidden, nil
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Checker: d.user,
|
||||
Path: filepath.Join(userscope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
}
|
||||
// Directories creation on POST.
|
||||
if strings.HasSuffix(path, "/") {
|
||||
@@ -174,6 +189,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
fileInfo, err := files.FileInfoFaster(fileOpts)
|
||||
if err == nil {
|
||||
if r.URL.Query().Get("override") != "true" {
|
||||
logger.Debug(fmt.Sprintf("Resource already exists: %v", fileInfo.RealPath))
|
||||
return http.StatusConflict, nil
|
||||
}
|
||||
|
||||
@@ -210,9 +226,11 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
source = config.Server.DefaultSource.Name
|
||||
}
|
||||
if !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
|
||||
// Decode the URL-encoded path
|
||||
@@ -220,21 +238,19 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
if !d.user.Perm.Modify || !d.user.Check(path) {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Only allow PUT for files.
|
||||
if strings.HasSuffix(path, "/") {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
fileOpts := files.FileOptions{
|
||||
Path: filepath.Join(d.user.Scope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
Checker: d.user,
|
||||
Path: filepath.Join(userscope, path),
|
||||
Source: source,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
}
|
||||
err = files.WriteFile(fileOpts, r.Body)
|
||||
return errToStatus(err), err
|
||||
@@ -261,10 +277,11 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
// TODO source := r.URL.Query().Get("source")
|
||||
action := r.URL.Query().Get("action")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
|
||||
if !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
|
||||
encodedFrom := r.URL.Query().Get("from")
|
||||
// Decode the URL-encoded path
|
||||
src, err := url.QueryUnescape(encodedFrom)
|
||||
@@ -276,21 +293,52 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
if !d.user.Check(src) || !d.user.Check(dst) {
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user rules deny access to source or destination")
|
||||
|
||||
splitSrc := strings.Split(src, "::")
|
||||
if len(splitSrc) <= 1 {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid source path: %v", src)
|
||||
}
|
||||
srcIndex := splitSrc[0]
|
||||
src = splitSrc[1]
|
||||
|
||||
splitDst := strings.Split(dst, "::")
|
||||
if len(splitDst) <= 1 {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid destination path: %v", dst)
|
||||
}
|
||||
dstIndex := splitDst[0]
|
||||
dst = splitDst[1]
|
||||
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: source or destination is attempting to modify root")
|
||||
}
|
||||
|
||||
idx := files.GetIndex(source)
|
||||
// check target dir exists
|
||||
parentDir, _, err := idx.GetRealPath(d.user.Scope, filepath.Dir(dst))
|
||||
userscopeDst, err := settings.GetScopeFromSourceName(d.user.Scopes, dstIndex)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
userscopeSrc, err := settings.GetScopeFromSourceName(d.user.Scopes, srcIndex)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
|
||||
idx := files.GetIndex(dstIndex)
|
||||
if idx == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source %s not found", dstIndex)
|
||||
}
|
||||
// check target dir exists
|
||||
parentDir, _, err := idx.GetRealPath(userscopeDst, filepath.Dir(dst))
|
||||
if err != nil {
|
||||
logger.Debug(fmt.Sprintf("Could not get real path for parent dir: %v %v %v", userscopeDst, filepath.Dir(dst), err))
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
realDest := parentDir + "/" + filepath.Base(dst)
|
||||
realSrc, isSrcDir, err := idx.GetRealPath(d.user.Scope, src)
|
||||
|
||||
idx2 := files.GetIndex(srcIndex)
|
||||
if idx2 == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source %s not found", srcIndex)
|
||||
}
|
||||
|
||||
realSrc, isSrcDir, err := idx2.GetRealPath(userscopeSrc, src)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
@@ -303,9 +351,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
if overwrite && !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user does not have permission to overwrite file")
|
||||
}
|
||||
err = d.Runner.RunHook(func() error {
|
||||
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)
|
||||
}, action, realSrc, realDest, d.user)
|
||||
err = patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, srcIndex, dstIndex)
|
||||
if err != nil {
|
||||
logger.Debug(fmt.Sprintf("Could not run patch action. src=%v dst=%v err=%v", realSrc, realDest, err))
|
||||
}
|
||||
@@ -335,34 +381,34 @@ func delThumbs(ctx context.Context, fileCache FileCache, file files.ExtendedFile
|
||||
}
|
||||
}
|
||||
|
||||
func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool, index string) error {
|
||||
func patchAction(ctx context.Context, action, src, dst string, d *requestContext, fileCache FileCache, isSrcDir bool, srcIndex, destIndex string) error {
|
||||
switch action {
|
||||
case "copy":
|
||||
if !d.user.Perm.Create {
|
||||
if !d.user.Perm.Modify {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
err := files.CopyResource(index, src, dst, isSrcDir)
|
||||
err := files.CopyResource(srcIndex, destIndex, src, dst, isSrcDir)
|
||||
return err
|
||||
case "rename", "move":
|
||||
if !d.user.Perm.Rename {
|
||||
if !d.user.Perm.Modify {
|
||||
return errors.ErrPermissionDenied
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(files.FileOptions{
|
||||
Path: src,
|
||||
Source: index,
|
||||
Source: srcIndex,
|
||||
IsDir: isSrcDir,
|
||||
Modify: d.user.Perm.Modify,
|
||||
Expand: false,
|
||||
ReadHeader: false,
|
||||
Checker: d.user,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete thumbnails
|
||||
delThumbs(ctx, fileCache, fileInfo)
|
||||
return files.MoveResource(index, src, dst, isSrcDir)
|
||||
return files.MoveResource(srcIndex, destIndex, src, dst, isSrcDir)
|
||||
default:
|
||||
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||
}
|
||||
@@ -385,21 +431,23 @@ type DiskUsageResponse struct {
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/usage [get]
|
||||
func diskUsage(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
sourceName := r.URL.Query().Get("source")
|
||||
if sourceName == "" {
|
||||
sourceName = config.Server.DefaultSource.Name
|
||||
}
|
||||
value, ok := cache.DiskUsage.Get(source).(DiskUsageResponse)
|
||||
value, ok := cache.DiskUsage.Get(sourceName).(DiskUsageResponse)
|
||||
if ok {
|
||||
return renderJSON(w, r, &value)
|
||||
}
|
||||
|
||||
rootPath, ok := files.RootPaths[source]
|
||||
source, ok := config.Server.NameToSource[sourceName]
|
||||
if !ok {
|
||||
return 400, fmt.Errorf("bad source path provided: %v", source)
|
||||
return 403, fmt.Errorf("source '%s' either does not exist or user does not have permission to it", sourceName)
|
||||
}
|
||||
|
||||
usage, err := disk.UsageWithContext(r.Context(), rootPath)
|
||||
_, err := settings.GetScopeFromSourceName(d.user.Scopes, sourceName)
|
||||
if err != nil {
|
||||
return 403, fmt.Errorf("user doesn't have access to '%s', contact your administrator for access", sourceName)
|
||||
}
|
||||
usage, err := disk.UsageWithContext(r.Context(), source.Path)
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
}
|
||||
@@ -407,7 +455,7 @@ func diskUsage(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
||||
Total: usage.Total,
|
||||
Used: usage.Used,
|
||||
}
|
||||
cache.DiskUsage.Set(source, latestUsage)
|
||||
cache.DiskUsage.Set(sourceName, latestUsage)
|
||||
return renderJSON(w, r, &latestUsage)
|
||||
}
|
||||
|
||||
@@ -415,12 +463,16 @@ func inspectIndex(w http.ResponseWriter, r *http.Request) {
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
source = config.Server.DefaultSource.Name
|
||||
}
|
||||
// Decode the URL-encoded path
|
||||
path, _ := url.QueryUnescape(encodedPath)
|
||||
isNotDir := r.URL.Query().Get("isDir") == "false" // default to isDir true
|
||||
index := files.GetIndex(source)
|
||||
if index == nil {
|
||||
http.Error(w, "source not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
info, _ := index.GetReducedMetadata(path, !isNotDir)
|
||||
renderJSON(w, r, info) // nolint:errcheck
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func StartHttp(ctx context.Context, Service ImgService, storage *storage.Storage
|
||||
|
||||
// Public routes
|
||||
api.HandleFunc("GET /public/publicUser", publicUserGetHandler)
|
||||
api.HandleFunc("GET /public/dl", withHashFile(rawHandler))
|
||||
api.HandleFunc("GET /public/dl", withHashFile(publicRawHandler))
|
||||
api.HandleFunc("GET /public/share", withHashFile(publicShareHandler))
|
||||
|
||||
// Settings routes
|
||||
|
||||
+11
-2
@@ -1,10 +1,12 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// searchHandler handles search requests for files based on the provided query.
|
||||
@@ -55,14 +57,21 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
query := r.URL.Query().Get("query")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = "default"
|
||||
source = config.Server.DefaultSource.Name
|
||||
}
|
||||
searchScope := strings.TrimPrefix(r.URL.Query().Get("scope"), ".")
|
||||
searchScope = strings.TrimPrefix(searchScope, "/")
|
||||
// Retrieve the User-Agent and X-Auth headers from the request
|
||||
sessionId := r.Header.Get("SessionId")
|
||||
index := files.GetIndex(source)
|
||||
userScope := strings.TrimPrefix(d.user.Scope, ".")
|
||||
if index == nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("index not found for source %s", source)
|
||||
}
|
||||
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
userScope := strings.TrimPrefix(userscope, ".")
|
||||
combinedScope := strings.TrimPrefix(userScope+"/"+searchScope, "/")
|
||||
|
||||
// Perform the search using the provided query and user scope
|
||||
|
||||
+21
-16
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
type settingsData struct {
|
||||
@@ -13,35 +12,43 @@ type settingsData struct {
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
Defaults settings.UserDefaults `json:"defaults"`
|
||||
Rules []users.Rule `json:"rules"`
|
||||
Frontend settings.Frontend `json:"frontend"`
|
||||
Commands map[string][]string `json:"commands"`
|
||||
}
|
||||
|
||||
// settingsGetHandler retrieves the current system settings.
|
||||
// @Summary Get system settings
|
||||
// @Description Returns the current configuration settings for signup, user directories, rules, frontend, and commands.
|
||||
// @Description Returns the current configuration settings for signup, user directories, rules, frontend.
|
||||
// @Tags Settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param property query string false "Property to retrieve"
|
||||
// @Success 200 {object} settingsData "System settings data"
|
||||
// @Router /api/settings [get]
|
||||
func settingsGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
data := &settingsData{
|
||||
Signup: config.Auth.Signup,
|
||||
CreateUserDir: config.Server.CreateUserDir,
|
||||
UserHomeBasePath: config.Server.UserHomeBasePath,
|
||||
Defaults: config.UserDefaults,
|
||||
Rules: config.Rules,
|
||||
Frontend: config.Frontend,
|
||||
property := r.URL.Query().Get("property")
|
||||
if property != "" {
|
||||
// get property by name
|
||||
switch property {
|
||||
case "userDefaults":
|
||||
return renderJSON(w, r, config.UserDefaults)
|
||||
case "frontend":
|
||||
return renderJSON(w, r, config.Frontend)
|
||||
case "auth":
|
||||
return renderJSON(w, r, config.Auth)
|
||||
case "server":
|
||||
return renderJSON(w, r, config.Server)
|
||||
case "sources":
|
||||
return renderJSON(w, r, config.Server.Sources)
|
||||
default:
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
}
|
||||
|
||||
return renderJSON(w, r, data)
|
||||
return renderJSON(w, r, config)
|
||||
}
|
||||
|
||||
// settingsPutHandler updates the system settings.
|
||||
// @Summary Update system settings
|
||||
// @Description Updates the system configuration settings for signup, user directories, rules, frontend, and commands.
|
||||
// @Description Updates the system configuration settings for signup, user directories, rules, frontend.
|
||||
// @Tags Settings
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -57,10 +64,8 @@ func settingsPutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
config.Server.CreateUserDir = req.CreateUserDir
|
||||
config.Server.UserHomeBasePath = req.UserHomeBasePath
|
||||
config.UserDefaults = req.Defaults
|
||||
config.Rules = req.Rules
|
||||
config.Frontend = req.Frontend
|
||||
config.Auth.Signup = req.Signup
|
||||
err = store.Settings.Save(config)
|
||||
|
||||
+15
-2
@@ -13,6 +13,7 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/share"
|
||||
)
|
||||
|
||||
@@ -59,12 +60,17 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param path query string true "Resource path for which to retrieve share links"
|
||||
// @Param source query string true "Source name for share links"
|
||||
// @Success 200 {array} share.Link "List of share links for the specified path"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/share [get]
|
||||
func shareGetsHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.URL.Query().Get("path")
|
||||
s, err := store.Share.Gets(path, d.user.ID)
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = settings.Config.Server.DefaultSource.Name
|
||||
}
|
||||
s, err := store.Share.Gets(path, source, d.user.ID)
|
||||
if err == errors.ErrNotExist {
|
||||
return renderJSON(w, r, []*share.Link{})
|
||||
}
|
||||
@@ -108,6 +114,8 @@ func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body share.CreateBody true "Share link creation parameters"
|
||||
// @Param path path string true "Source Path of the files to share"
|
||||
// @Param source path string true "Source name of the files to share"
|
||||
// @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"
|
||||
@@ -159,16 +167,21 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
var token string
|
||||
if len(hash) > 0 {
|
||||
tokenBuffer := make([]byte, 24) //nolint:gomnd
|
||||
if _, err := rand.Read(tokenBuffer); err != nil {
|
||||
if _, err = rand.Read(tokenBuffer); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
token = base64.URLEncoding.EncodeToString(tokenBuffer)
|
||||
stringHash = string(hash)
|
||||
}
|
||||
path := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
if source == "" {
|
||||
source = config.Server.DefaultSource.Name
|
||||
}
|
||||
s = &share.Link{
|
||||
Path: path,
|
||||
Hash: secure_hash,
|
||||
Source: source,
|
||||
Expire: expire,
|
||||
UserID: d.user.ID,
|
||||
PasswordHash: stringHash,
|
||||
|
||||
+11
-85
@@ -5,28 +5,15 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/storage"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
var (
|
||||
NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules"}
|
||||
)
|
||||
|
||||
// SortingSettings represents the sorting settings.
|
||||
type Sorting struct {
|
||||
By string `json:"by"`
|
||||
Asc bool `json:"asc"`
|
||||
}
|
||||
type UserRequest struct {
|
||||
What string `json:"what"`
|
||||
Which []string `json:"which"`
|
||||
@@ -56,15 +43,14 @@ func userGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
givenUserId = d.user.ID
|
||||
} else if givenUserIdString == "" {
|
||||
|
||||
userList, err := store.Users.Gets(files.RootPaths["default"])
|
||||
userList, err := store.Users.Gets()
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
selfUserList := []*users.User{}
|
||||
for _, u := range userList {
|
||||
u.Password = ""
|
||||
u.ApiKeys = nil
|
||||
prepForFrontend(u)
|
||||
if u.ID == d.user.ID {
|
||||
selfUserList = append(selfUserList, u)
|
||||
}
|
||||
@@ -88,22 +74,21 @@ func userGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
}
|
||||
|
||||
// Fetch the user details
|
||||
u, err := store.Users.Get(files.RootPaths["default"], givenUserId)
|
||||
u, err := store.Users.Get(givenUserId)
|
||||
if err == errors.ErrNotExist {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
prepForFrontend(u)
|
||||
return renderJSON(w, r, u)
|
||||
}
|
||||
|
||||
// Remove the password from the response if the user is not an admin
|
||||
func prepForFrontend(u *users.User) {
|
||||
u.Password = ""
|
||||
u.ApiKeys = nil
|
||||
if !d.user.Perm.Admin {
|
||||
u.Scope = ""
|
||||
}
|
||||
|
||||
return renderJSON(w, r, u)
|
||||
u.Scopes = settings.ConvertToFrontendScopes(u.Scopes)
|
||||
}
|
||||
|
||||
// userDeleteHandler deletes a user by ID.
|
||||
@@ -157,26 +142,17 @@ func usersPostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if !d.user.Perm.Admin {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Validate the user's scope
|
||||
idx := files.GetIndex("default")
|
||||
_, _, err := idx.GetRealPath(d.user.Scope)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// Read the JSON body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Parse the JSON into the UserRequest struct
|
||||
var req UserRequest
|
||||
if err = json.Unmarshal(body, &req); err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
if len(req.Which) != 0 {
|
||||
return http.StatusBadRequest, nil
|
||||
@@ -217,13 +193,6 @@ func userPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
// Validate the user's scope
|
||||
idx := files.GetIndex("default")
|
||||
_, _, err := idx.GetRealPath(d.user.Scope)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// Read the JSON body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -237,51 +206,8 @@ func userPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
// If `Which` is not specified, default to updating all fields
|
||||
if len(req.Which) == 0 || req.Which[0] == "all" {
|
||||
req.Which = []string{}
|
||||
v := reflect.ValueOf(req.Data)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
t := v.Type()
|
||||
|
||||
// Dynamically populate fields to update
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if field.Name == "Password" && req.Data.Password != "" {
|
||||
req.Which = append(req.Which, field.Name)
|
||||
} else if field.Name != "Password" && field.Name != "Fs" {
|
||||
req.Which = append(req.Which, field.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the fields to update
|
||||
for _, field := range req.Which {
|
||||
// Title case field names
|
||||
field = cases.Title(language.English, cases.NoLower).String(field)
|
||||
|
||||
// Handle password update
|
||||
if field == "Password" {
|
||||
if !d.user.Perm.Admin && d.user.LockPassword {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
req.Data.Password, err = users.HashPwd(req.Data.Password)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent non-admins from modifying certain fields
|
||||
for _, restrictedField := range NonModifiableFieldsForNonAdmin {
|
||||
if !d.user.Perm.Admin && field == restrictedField {
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// Perform the user update
|
||||
err = store.Users.Update(req.Data, req.Which...)
|
||||
err = store.Users.Update(req.Data, d.user.Perm.Admin, req.Which...)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"unicode"
|
||||
|
||||
shlex "github.com/flynn/go-shlex"
|
||||
)
|
||||
|
||||
const (
|
||||
osWindows = "windows"
|
||||
osLinux = "linux"
|
||||
)
|
||||
|
||||
var runtimeGoos = runtime.GOOS
|
||||
|
||||
// SplitCommandAndArgs takes a command string and parses it shell-style into the
|
||||
// command and its separate arguments.
|
||||
func SplitCommandAndArgs(command string) (cmd string, args []string, err error) {
|
||||
var parts []string
|
||||
|
||||
if runtimeGoos == osWindows {
|
||||
parts = parseWindowsCommand(command) // parse it Windows-style
|
||||
} else {
|
||||
parts, err = parseUnixCommand(command) // parse it Unix-style
|
||||
if err != nil {
|
||||
err = errors.New("error parsing command: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
err = errors.New("no command contained in '" + command + "'")
|
||||
return
|
||||
}
|
||||
|
||||
cmd = parts[0]
|
||||
if len(parts) > 1 {
|
||||
args = parts[1:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseUnixCommand parses a unix style command line and returns the
|
||||
// command and its arguments or an error
|
||||
func parseUnixCommand(cmd string) ([]string, error) {
|
||||
return shlex.Split(cmd)
|
||||
}
|
||||
|
||||
// parseWindowsCommand parses windows command lines and
|
||||
// returns the command and the arguments as an array. It
|
||||
// should be able to parse commonly used command lines.
|
||||
// Only basic syntax is supported:
|
||||
// - spaces in double quotes are not token delimiters
|
||||
// - double quotes are escaped by either backspace or another double quote
|
||||
// - except for the above case backspaces are path separators (not special)
|
||||
//
|
||||
// Many sources point out that escaping quotes using backslash can be unsafe.
|
||||
// Use two double quotes when possible. (Source: http://stackoverflow.com/a/31413730/2616179 )
|
||||
//
|
||||
// This function has to be used on Windows instead
|
||||
// of the shlex package because this function treats backslash
|
||||
// characters properly.
|
||||
func parseWindowsCommand(cmd string) []string {
|
||||
const backslash = '\\'
|
||||
const quote = '"'
|
||||
|
||||
var parts []string
|
||||
var part string
|
||||
var inQuotes bool
|
||||
var lastRune rune
|
||||
|
||||
for i, ch := range cmd {
|
||||
if i != 0 {
|
||||
lastRune = rune(cmd[i-1])
|
||||
}
|
||||
|
||||
if ch == backslash {
|
||||
// put it in the part - for now we don't know if it's an
|
||||
// escaping char or path separator
|
||||
part += string(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == quote {
|
||||
if lastRune == backslash {
|
||||
// remove the backslash from the part and add the escaped quote instead
|
||||
part = part[:len(part)-1]
|
||||
part += string(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if lastRune == quote {
|
||||
// revert the last change of the inQuotes state
|
||||
// it was an escaping quote
|
||||
inQuotes = !inQuotes
|
||||
part += string(ch)
|
||||
continue
|
||||
}
|
||||
|
||||
// normal escaping quotes
|
||||
inQuotes = !inQuotes
|
||||
continue
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) && !inQuotes && len(part) > 0 {
|
||||
parts = append(parts, part)
|
||||
part = ""
|
||||
continue
|
||||
}
|
||||
|
||||
part += string(ch)
|
||||
}
|
||||
|
||||
if len(part) > 0 {
|
||||
parts = append(parts, part)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseUnixCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
// 0 - empty command
|
||||
{
|
||||
input: ``,
|
||||
expected: []string{},
|
||||
},
|
||||
// 1 - command without arguments
|
||||
{
|
||||
input: `command`,
|
||||
expected: []string{`command`},
|
||||
},
|
||||
// 2 - command with single argument
|
||||
{
|
||||
input: `command arg1`,
|
||||
expected: []string{`command`, `arg1`},
|
||||
},
|
||||
// 3 - command with multiple arguments
|
||||
{
|
||||
input: `command arg1 arg2`,
|
||||
expected: []string{`command`, `arg1`, `arg2`},
|
||||
},
|
||||
// 4 - command with single argument with space character - in quotes
|
||||
{
|
||||
input: `command "arg1 arg1"`,
|
||||
expected: []string{`command`, `arg1 arg1`},
|
||||
},
|
||||
// 5 - command with multiple spaces and tab character
|
||||
{
|
||||
input: "command arg1 arg2\targ3",
|
||||
expected: []string{`command`, `arg1`, `arg2`, `arg3`},
|
||||
},
|
||||
// 6 - command with single argument with space character - escaped with backspace
|
||||
{
|
||||
input: `command arg1\ arg2`,
|
||||
expected: []string{`command`, `arg1 arg2`},
|
||||
},
|
||||
// 7 - single quotes should escape special chars
|
||||
{
|
||||
input: `command 'arg1\ arg2'`,
|
||||
expected: []string{`command`, `arg1\ arg2`},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
|
||||
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
|
||||
actual, _ := parseUnixCommand(test.input)
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Errorf(errorPrefix+"Expected %d parts, got %d: %#v."+errorSuffix, len(test.expected), len(actual), actual)
|
||||
continue
|
||||
}
|
||||
for j := 0; j < len(actual); j++ {
|
||||
if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart {
|
||||
t.Errorf(errorPrefix+"Expected: %v Actual: %v (index %d)."+errorSuffix, expectedPart, actualPart, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWindowsCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{ // 0 - empty command - do not fail
|
||||
input: ``,
|
||||
expected: []string{},
|
||||
},
|
||||
{ // 1 - cmd without args
|
||||
input: `cmd`,
|
||||
expected: []string{`cmd`},
|
||||
},
|
||||
{ // 2 - multiple args
|
||||
input: `cmd arg1 arg2`,
|
||||
expected: []string{`cmd`, `arg1`, `arg2`},
|
||||
},
|
||||
{ // 3 - multiple args with space
|
||||
input: `cmd "combined arg" arg2`,
|
||||
expected: []string{`cmd`, `combined arg`, `arg2`},
|
||||
},
|
||||
{ // 4 - path without spaces
|
||||
input: `mkdir C:\Windows\foo\bar`,
|
||||
expected: []string{`mkdir`, `C:\Windows\foo\bar`},
|
||||
},
|
||||
{ // 5 - command with space in quotes
|
||||
input: `"command here"`,
|
||||
expected: []string{`command here`},
|
||||
},
|
||||
{ // 6 - argument with escaped quotes (two quotes)
|
||||
input: `cmd ""arg""`,
|
||||
expected: []string{`cmd`, `"arg"`},
|
||||
},
|
||||
{ // 7 - argument with escaped quotes (backslash)
|
||||
input: `cmd \"arg\"`,
|
||||
expected: []string{`cmd`, `"arg"`},
|
||||
},
|
||||
{ // 8 - two quotes (escaped) inside an inQuote element
|
||||
input: `cmd "a ""quoted value"`,
|
||||
expected: []string{`cmd`, `a "quoted value`},
|
||||
},
|
||||
// TODO - see how many quotes are displayed if we use "", """, """""""
|
||||
{ // 9 - two quotes outside an inQuote element
|
||||
input: `cmd a ""quoted value`,
|
||||
expected: []string{`cmd`, `a`, `"quoted`, `value`},
|
||||
},
|
||||
{ // 10 - path with space in quotes
|
||||
input: `mkdir "C:\directory name\foobar"`,
|
||||
expected: []string{`mkdir`, `C:\directory name\foobar`},
|
||||
},
|
||||
{ // 11 - space without quotes
|
||||
input: `mkdir C:\ space`,
|
||||
expected: []string{`mkdir`, `C:\`, `space`},
|
||||
},
|
||||
{ // 12 - space in quotes
|
||||
input: `mkdir "C:\ space"`,
|
||||
expected: []string{`mkdir`, `C:\ space`},
|
||||
},
|
||||
{ // 13 - UNC
|
||||
input: `mkdir \\?\C:\Users`,
|
||||
expected: []string{`mkdir`, `\\?\C:\Users`},
|
||||
},
|
||||
{ // 14 - UNC with space
|
||||
input: `mkdir "\\?\C:\Program Files"`,
|
||||
expected: []string{`mkdir`, `\\?\C:\Program Files`},
|
||||
},
|
||||
|
||||
{ // 15 - unclosed quotes - treat as if the path ends with quote
|
||||
input: `mkdir "c:\Program files`,
|
||||
expected: []string{`mkdir`, `c:\Program files`},
|
||||
},
|
||||
{ // 16 - quotes used inside the argument
|
||||
input: `mkdir "c:\P"rogra"m f"iles`,
|
||||
expected: []string{`mkdir`, `c:\Program files`},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
|
||||
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
|
||||
|
||||
actual := parseWindowsCommand(test.input)
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Errorf(errorPrefix+"Expected %d parts, got %d: %#v."+errorSuffix, len(test.expected), len(actual), actual)
|
||||
continue
|
||||
}
|
||||
for j := 0; j < len(actual); j++ {
|
||||
if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart {
|
||||
t.Errorf(errorPrefix+"Expected: %v Actual: %v (index %d)."+errorSuffix, expectedPart, actualPart, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitCommandAndArgs(t *testing.T) {
|
||||
// force linux parsing. It's more robust and covers error cases
|
||||
runtimeGoos = osLinux
|
||||
defer func() {
|
||||
runtimeGoos = runtime.GOOS
|
||||
}()
|
||||
|
||||
var parseErrorContent = "error parsing command:"
|
||||
var noCommandErrContent = "no command contained in"
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expectedCommand string
|
||||
expectedArgs []string
|
||||
expectedErrContent string
|
||||
}{
|
||||
// 0 - empty command
|
||||
{
|
||||
input: ``,
|
||||
expectedCommand: ``,
|
||||
expectedArgs: nil,
|
||||
expectedErrContent: noCommandErrContent,
|
||||
},
|
||||
// 1 - command without arguments
|
||||
{
|
||||
input: `command`,
|
||||
expectedCommand: `command`,
|
||||
expectedArgs: nil,
|
||||
expectedErrContent: ``,
|
||||
},
|
||||
// 2 - command with single argument
|
||||
{
|
||||
input: `command arg1`,
|
||||
expectedCommand: `command`,
|
||||
expectedArgs: []string{`arg1`},
|
||||
expectedErrContent: ``,
|
||||
},
|
||||
// 3 - command with multiple arguments
|
||||
{
|
||||
input: `command arg1 arg2`,
|
||||
expectedCommand: `command`,
|
||||
expectedArgs: []string{`arg1`, `arg2`},
|
||||
expectedErrContent: ``,
|
||||
},
|
||||
// 4 - command with unclosed quotes
|
||||
{
|
||||
input: `command "arg1 arg2`,
|
||||
expectedCommand: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrContent: parseErrorContent,
|
||||
},
|
||||
// 5 - command with unclosed quotes
|
||||
{
|
||||
input: `command 'arg1 arg2"`,
|
||||
expectedCommand: "",
|
||||
expectedArgs: nil,
|
||||
expectedErrContent: parseErrorContent,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
|
||||
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
|
||||
actualCommand, actualArgs, actualErr := SplitCommandAndArgs(test.input)
|
||||
|
||||
// test if error matches expectation
|
||||
if test.expectedErrContent != "" {
|
||||
if actualErr == nil {
|
||||
t.Errorf(errorPrefix+"Expected error with content [%s], found no error."+errorSuffix, test.expectedErrContent)
|
||||
} else if !strings.Contains(actualErr.Error(), test.expectedErrContent) {
|
||||
t.Errorf(errorPrefix+"Expected error with content [%s], found [%v]."+errorSuffix, test.expectedErrContent, actualErr)
|
||||
}
|
||||
} else if actualErr != nil {
|
||||
t.Errorf(errorPrefix+"Expected no error, found [%v]."+errorSuffix, actualErr)
|
||||
}
|
||||
|
||||
// test if command matches
|
||||
if test.expectedCommand != actualCommand {
|
||||
t.Errorf(errorPrefix+"Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand)
|
||||
}
|
||||
|
||||
// test if arguments match
|
||||
if len(test.expectedArgs) != len(actualArgs) {
|
||||
t.Errorf(errorPrefix+"Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs)
|
||||
} else {
|
||||
// test args only if the count matches.
|
||||
for j, actualArg := range actualArgs {
|
||||
expectedArg := test.expectedArgs[j]
|
||||
if actualArg != expectedArg {
|
||||
t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSplitCommandAndArgs() {
|
||||
var commandLine string
|
||||
var command string
|
||||
var args []string
|
||||
|
||||
// just for the test - change GOOS and reset it at the end of the test
|
||||
runtimeGoos = osWindows
|
||||
defer func() {
|
||||
runtimeGoos = runtime.GOOS
|
||||
}()
|
||||
|
||||
commandLine = `mkdir /P "C:\Program Files"`
|
||||
command, args, _ = SplitCommandAndArgs(commandLine)
|
||||
|
||||
fmt.Printf("Windows: %s: %s [%s]\n", commandLine, command, strings.Join(args, ","))
|
||||
|
||||
// set GOOS to linux
|
||||
runtimeGoos = osLinux
|
||||
|
||||
commandLine = `mkdir -p /path/with\ space`
|
||||
command, args, _ = SplitCommandAndArgs(commandLine)
|
||||
|
||||
fmt.Printf("Linux: %s: %s [%s]\n", commandLine, command, strings.Join(args, ","))
|
||||
|
||||
// Output:
|
||||
// Windows: mkdir /P "C:\Program Files": mkdir [/P,C:\Program Files]
|
||||
// Linux: mkdir -p /path/with\ space: mkdir [-p,/path/with space]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
)
|
||||
|
||||
// ParseCommand parses the command taking in account if the current
|
||||
// instance uses a shell to run the commands or just calls the binary
|
||||
// directyly.
|
||||
func ParseCommand(s *settings.Settings, raw string) ([]string, error) {
|
||||
var command []string
|
||||
|
||||
if len(s.Shell) == 0 {
|
||||
cmd, args, err := SplitCommandAndArgs(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = exec.LookPath(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
command = append(command, cmd)
|
||||
command = append(command, args...)
|
||||
} else {
|
||||
command = append(s.Shell, raw) //nolint:gocritic
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// Runner is a commands runner.
|
||||
type Runner struct {
|
||||
Enabled bool
|
||||
*settings.Settings
|
||||
}
|
||||
|
||||
// RunHook runs the hooks for the before and after event.
|
||||
func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
|
||||
idx := files.GetIndex("default")
|
||||
path, _, _ = idx.GetRealPath(user.Scope, path)
|
||||
dst, _, _ = idx.GetRealPath(user.Scope, dst)
|
||||
|
||||
if r.Enabled {
|
||||
if val, ok := r.Settings.Commands["before_"+evt]; ok {
|
||||
for _, command := range val {
|
||||
err := r.exec(command, "before_"+evt, path, dst, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Enabled {
|
||||
if val, ok := r.Settings.Commands["after_"+evt]; ok {
|
||||
for _, command := range val {
|
||||
err := r.exec(command, "after_"+evt, path, dst, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error {
|
||||
blocking := true
|
||||
|
||||
if strings.HasSuffix(raw, "&") {
|
||||
blocking = false
|
||||
raw = strings.TrimSpace(strings.TrimSuffix(raw, "&"))
|
||||
}
|
||||
|
||||
command, err := ParseCommand(r.Settings, raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envMapping := func(key string) string {
|
||||
switch key {
|
||||
case "FILE":
|
||||
return path
|
||||
case "SCOPE":
|
||||
return user.Scope
|
||||
case "TRIGGER":
|
||||
return evt
|
||||
case "USERNAME":
|
||||
return user.Username
|
||||
case "DESTINATION":
|
||||
return dst
|
||||
default:
|
||||
return os.Getenv(key)
|
||||
}
|
||||
}
|
||||
for i, arg := range command {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
command[i] = os.Expand(arg, envMapping)
|
||||
}
|
||||
|
||||
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) //nolint:gocritic
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst))
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if !blocking {
|
||||
log.Printf("Nonblocking Command: \"%s\"", strings.Join(command, " "))
|
||||
defer func() {
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Nonblocking Command \"%s\" failed: %s", strings.Join(command, " "), err)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
log.Printf("Blocking Command: \"%s\"", strings.Join(command, " "))
|
||||
return cmd.Run()
|
||||
}
|
||||
+227
-88
@@ -5,85 +5,31 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/version"
|
||||
)
|
||||
|
||||
var Config Settings
|
||||
|
||||
func Initialize(configFile string) {
|
||||
yamlData, err := loadConfigFile(configFile)
|
||||
if err != nil && configFile != "config.yaml" {
|
||||
logger.Fatal("Could not load specified config file: " + err.Error())
|
||||
}
|
||||
err := loadConfigWithDefaults(configFile)
|
||||
if err != nil {
|
||||
logger.Warning(fmt.Sprintf("Could not load config file '%v', using default settings: %v", configFile, err))
|
||||
}
|
||||
Config = setDefaults()
|
||||
err = yaml.Unmarshal(yamlData, &Config)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
}
|
||||
if len(Config.Server.Logging) == 0 {
|
||||
Config.Server.Logging = []LogConfig{
|
||||
{
|
||||
Output: "stdout",
|
||||
},
|
||||
}
|
||||
}
|
||||
for _, logConfig := range Config.Server.Logging {
|
||||
err = logger.SetupLogger(
|
||||
logConfig.Output,
|
||||
logConfig.Levels,
|
||||
logConfig.ApiLevels,
|
||||
logConfig.NoColors,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Failed to set up logger:", err)
|
||||
}
|
||||
}
|
||||
if Config.Auth.Method != "" {
|
||||
logger.Warning("The `auth.method` setting is deprecated and will be removed in a future version. Please use `auth.methods` instead.")
|
||||
}
|
||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||
// Convert relative path to absolute path
|
||||
if len(Config.Server.Sources) > 0 {
|
||||
if Config.Server.Root != "" {
|
||||
logger.Warning("`server.root` is configured but will be ignored in favor of `server.sources`")
|
||||
}
|
||||
// TODO allow multiple sources not named default
|
||||
for _, source := range Config.Server.Sources {
|
||||
realPath, err2 := filepath.Abs(source.Path)
|
||||
if err2 != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||
}
|
||||
source.Path = realPath
|
||||
source.Name = "default"
|
||||
Config.Server.Sources = []Source{source} // temporary set only one source
|
||||
}
|
||||
} else {
|
||||
realPath, err2 := filepath.Abs(Config.Server.Root)
|
||||
if err2 != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error getting source path: %v", err2))
|
||||
}
|
||||
Config.Server.Sources = []Source{
|
||||
{
|
||||
Name: "default",
|
||||
Path: realPath,
|
||||
},
|
||||
}
|
||||
logger.Fatal(err.Error())
|
||||
}
|
||||
setupLogging()
|
||||
setupAuth()
|
||||
setupSources()
|
||||
setupBaseURL()
|
||||
setupFrontend()
|
||||
}
|
||||
|
||||
baseurl := strings.Trim(Config.Server.BaseURL, "/")
|
||||
if baseurl == "" {
|
||||
Config.Server.BaseURL = "/"
|
||||
} else {
|
||||
Config.Server.BaseURL = "/" + baseurl + "/"
|
||||
}
|
||||
func setupFrontend() {
|
||||
if !Config.Frontend.DisableDefaultLinks {
|
||||
Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{
|
||||
Text: fmt.Sprintf("(%v)", version.Version),
|
||||
@@ -97,25 +43,153 @@ func Initialize(configFile string) {
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigFile(configFile string) ([]byte, error) {
|
||||
func getRealPath(path string) string {
|
||||
realPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("could not find configured source path: %v", err))
|
||||
}
|
||||
// check path exists
|
||||
if _, err = os.Stat(realPath); os.IsNotExist(err) {
|
||||
logger.Fatal(fmt.Sprintf("configured source path does not exist: %v", realPath))
|
||||
}
|
||||
return realPath
|
||||
}
|
||||
|
||||
func setupSources() {
|
||||
if len(Config.Server.Sources) == 0 {
|
||||
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 {
|
||||
realPath := getRealPath(source.Path)
|
||||
name := utils.GetLastComponent(realPath)
|
||||
source.Path = realPath // use absolute path
|
||||
if source.Name == "" {
|
||||
_, ok := Config.Server.SourceMap[source.Path]
|
||||
if ok {
|
||||
source.Name = name + fmt.Sprintf("-%v", k)
|
||||
} else {
|
||||
source.Name = name
|
||||
}
|
||||
if Config.Server.DefaultSource.Path == "" {
|
||||
Config.Server.DefaultSource = source
|
||||
}
|
||||
}
|
||||
Config.Server.SourceMap[source.Path] = source
|
||||
Config.Server.NameToSource[source.Name] = source
|
||||
}
|
||||
}
|
||||
// clean up the in memory source list to be accurate and unique
|
||||
sourceList := []Source{}
|
||||
defaultScopes := []users.SourceScope{}
|
||||
allSourceNames := []string{}
|
||||
first := true
|
||||
for _, sourcePathOnly := range Config.Server.Sources {
|
||||
realPath := getRealPath(sourcePathOnly.Path)
|
||||
source, ok := Config.Server.SourceMap[realPath]
|
||||
if ok && !slices.Contains(allSourceNames, source.Name) {
|
||||
if first {
|
||||
source.Config.DefaultEnabled = true
|
||||
Config.Server.SourceMap[source.Path] = source
|
||||
Config.Server.NameToSource[source.Name] = source
|
||||
Config.Server.DefaultSource = source
|
||||
}
|
||||
first = false
|
||||
sourceList = append(sourceList, source)
|
||||
if source.Config.DefaultEnabled {
|
||||
Config.Server.DefaultSource = source
|
||||
defaultScopes = append(defaultScopes, users.SourceScope{
|
||||
Name: source.Path,
|
||||
Scope: source.Config.DefaultUserScope,
|
||||
})
|
||||
}
|
||||
allSourceNames = append(allSourceNames, source.Name)
|
||||
} else {
|
||||
logger.Warning(fmt.Sprintf("source %v is not configured correctly, skipping", sourcePathOnly.Path))
|
||||
}
|
||||
}
|
||||
Config.UserDefaults.DefaultScopes = defaultScopes
|
||||
Config.Server.Sources = sourceList
|
||||
}
|
||||
|
||||
func setupBaseURL() {
|
||||
baseurl := strings.Trim(Config.Server.BaseURL, "/")
|
||||
if baseurl == "" {
|
||||
Config.Server.BaseURL = "/"
|
||||
} else {
|
||||
Config.Server.BaseURL = "/" + baseurl + "/"
|
||||
}
|
||||
}
|
||||
|
||||
func setupAuth() {
|
||||
if Config.Auth.Method != "" {
|
||||
logger.Warning("The `auth.method` setting is deprecated and will be removed in a future version. Please use `auth.methods` instead.")
|
||||
}
|
||||
Config.UserDefaults.Perm = Config.UserDefaults.Permissions
|
||||
if Config.Auth.Methods.PasswordAuth.Enabled {
|
||||
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "Password")
|
||||
}
|
||||
if Config.Auth.Methods.ProxyAuth.Enabled {
|
||||
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "Proxy")
|
||||
}
|
||||
if Config.Auth.Methods.NoAuth {
|
||||
logger.Warning("Configured with no authentication, this is not recommended.")
|
||||
Config.Auth.AuthMethods = []string{"Disabled"}
|
||||
}
|
||||
// use password auth as default if no auth methods are set
|
||||
if len(Config.Auth.AuthMethods) == 0 {
|
||||
Config.Auth.Methods.PasswordAuth.Enabled = true
|
||||
Config.Auth.AuthMethods = append(Config.Auth.AuthMethods, "Password")
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogging() {
|
||||
if len(Config.Server.Logging) == 0 {
|
||||
Config.Server.Logging = []LogConfig{
|
||||
{
|
||||
Output: "stdout",
|
||||
},
|
||||
}
|
||||
}
|
||||
for _, logConfig := range Config.Server.Logging {
|
||||
err := logger.SetupLogger(
|
||||
logConfig.Output,
|
||||
logConfig.Levels,
|
||||
logConfig.ApiLevels,
|
||||
logConfig.NoColors,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Failed to set up logger:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigWithDefaults(configFile string) error {
|
||||
// Open and read the YAML file
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
|
||||
stat, err := yamlFile.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
yamlData := make([]byte, stat.Size())
|
||||
_, err = yamlFile.Read(yamlData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err != nil && configFile != "config.yaml" {
|
||||
return fmt.Errorf("could not load specified config file: " + err.Error())
|
||||
}
|
||||
return yamlData, nil
|
||||
if err != nil {
|
||||
logger.Warning(fmt.Sprintf("Could not load config file '%v', using default settings: %v", configFile, err))
|
||||
}
|
||||
Config = setDefaults()
|
||||
err = yaml.Unmarshal(yamlData, &Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshaling YAML data: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDefaults() Settings {
|
||||
@@ -128,7 +202,8 @@ func setDefaults() Settings {
|
||||
NumImageProcessors: 4,
|
||||
BaseURL: "",
|
||||
Database: "database.db",
|
||||
Root: ".",
|
||||
SourceMap: map[string]Source{},
|
||||
NameToSource: map[string]Source{},
|
||||
},
|
||||
Auth: Auth{
|
||||
AdminUsername: "admin",
|
||||
@@ -139,14 +214,7 @@ func setDefaults() Settings {
|
||||
Host: "",
|
||||
},
|
||||
Methods: LoginMethods{
|
||||
ProxyAuth: ProxyAuthConfig{
|
||||
Enabled: false,
|
||||
CreateUser: false,
|
||||
Header: "",
|
||||
},
|
||||
NoAuth: false,
|
||||
PasswordAuth: PasswordAuthConfig{
|
||||
Enabled: true,
|
||||
MinLength: 5,
|
||||
},
|
||||
},
|
||||
@@ -157,7 +225,6 @@ func setDefaults() Settings {
|
||||
UserDefaults: UserDefaults{
|
||||
DisableOnlyOfficeExt: ".txt .csv .html",
|
||||
StickySidebar: true,
|
||||
Scope: ".",
|
||||
LockPassword: false,
|
||||
ShowHidden: false,
|
||||
DarkMode: true,
|
||||
@@ -166,15 +233,87 @@ func setDefaults() Settings {
|
||||
Locale: "en",
|
||||
GallerySize: 3,
|
||||
Permissions: users.Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
Modify: false,
|
||||
Delete: false,
|
||||
Share: false,
|
||||
Download: false,
|
||||
Admin: false,
|
||||
Api: false,
|
||||
Modify: false,
|
||||
Share: false,
|
||||
Admin: false,
|
||||
Api: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertToBackendScopes(scopes []users.SourceScope) ([]users.SourceScope, error) {
|
||||
if len(scopes) == 0 {
|
||||
return Config.UserDefaults.DefaultScopes, nil
|
||||
}
|
||||
newScopes := []users.SourceScope{}
|
||||
for _, scope := range scopes {
|
||||
// first check if its already a path name and keep it
|
||||
source, ok := Config.Server.SourceMap[scope.Name]
|
||||
if ok {
|
||||
newScopes = append(newScopes, users.SourceScope{
|
||||
Name: source.Path, // backend name is path
|
||||
Scope: scope.Scope,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// check if its the name of a source and convert it to a path
|
||||
source, ok = Config.Server.NameToSource[scope.Name]
|
||||
if !ok {
|
||||
return newScopes, fmt.Errorf("invalid scope for source %v", scope.Name)
|
||||
}
|
||||
newScopes = append(newScopes, users.SourceScope{
|
||||
Name: source.Path, // backend name is path
|
||||
Scope: scope.Scope,
|
||||
})
|
||||
}
|
||||
return newScopes, nil
|
||||
}
|
||||
|
||||
func ConvertToFrontendScopes(scopes []users.SourceScope) []users.SourceScope {
|
||||
newScopes := make([]users.SourceScope, 0, len(scopes)) // Preserve original order
|
||||
for _, scope := range scopes {
|
||||
if source, ok := Config.Server.SourceMap[scope.Name]; ok {
|
||||
// Replace scope.Name with source.Path while keeping the same Scope value
|
||||
newScopes = append(newScopes, users.SourceScope{
|
||||
Name: source.Name,
|
||||
Scope: scope.Scope,
|
||||
})
|
||||
}
|
||||
}
|
||||
return newScopes
|
||||
}
|
||||
|
||||
func HasSourceByPath(scopes []users.SourceScope, sourcePath string) bool {
|
||||
for _, scope := range scopes {
|
||||
if scope.Name == sourcePath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetScopeFromSourceName(scopes []users.SourceScope, sourceName string) (string, error) {
|
||||
source, ok := Config.Server.NameToSource[sourceName]
|
||||
if !ok {
|
||||
logger.Debug(fmt.Sprint("Could not get scope from source name: ", sourceName))
|
||||
return "", fmt.Errorf("source with name not found %v", sourceName)
|
||||
}
|
||||
for _, scope := range scopes {
|
||||
if scope.Name == source.Path {
|
||||
return scope.Scope, nil
|
||||
}
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("scope not found for source %v", sourceName))
|
||||
return "", fmt.Errorf("scope not found for source %v", sourceName)
|
||||
}
|
||||
|
||||
func GetScopeFromSourcePath(scopes []users.SourceScope, sourcePath string) (string, error) {
|
||||
for _, scope := range scopes {
|
||||
if scope.Name == sourcePath {
|
||||
return scope.Scope, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("scope not found for source %v", sourcePath)
|
||||
}
|
||||
@@ -22,26 +22,6 @@ func TestInitialize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_loadConfigFile(t *testing.T) {
|
||||
type args struct {
|
||||
configFile string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got, _ := loadConfigFile(tt.args.configFile); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("loadConfigFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_setDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
func TestSettings_MakeUserDir(t *testing.T) {
|
||||
type fields struct {
|
||||
Signup bool
|
||||
CreateUserDir bool
|
||||
UserHomeBasePath string
|
||||
Commands map[string][]string
|
||||
Shell []string
|
||||
AdminUsername string
|
||||
AdminPassword string
|
||||
Rules []users.Rule
|
||||
Server Server
|
||||
Auth Auth
|
||||
Frontend Frontend
|
||||
Users []UserDefaults
|
||||
UserDefaults UserDefaults
|
||||
}
|
||||
type args struct {
|
||||
username string
|
||||
userScope string
|
||||
serverRoot string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Settings{
|
||||
Commands: tt.fields.Commands,
|
||||
Shell: tt.fields.Shell,
|
||||
Rules: tt.fields.Rules,
|
||||
Server: tt.fields.Server,
|
||||
Auth: tt.fields.Auth,
|
||||
Frontend: tt.fields.Frontend,
|
||||
Users: tt.fields.Users,
|
||||
UserDefaults: tt.fields.UserDefaults,
|
||||
}
|
||||
got, err := s.MakeUserDir(tt.args.username, tt.args.userScope, tt.args.serverRoot)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Settings.MakeUserDir() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Settings.MakeUserDir() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_cleanUsername(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := cleanUsername(tt.args.s); got != tt.want {
|
||||
t.Errorf("cleanUsername() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,30 +29,28 @@ func GetSettingsConfig(nameType string, Value string) string {
|
||||
|
||||
func AdminPerms() users.Permissions {
|
||||
return users.Permissions{
|
||||
Create: true,
|
||||
Rename: true,
|
||||
Modify: true,
|
||||
Delete: true,
|
||||
Share: true,
|
||||
Download: true,
|
||||
Admin: true,
|
||||
Api: true,
|
||||
Modify: true,
|
||||
Share: true,
|
||||
Admin: true,
|
||||
Api: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply applies the default options to a user.
|
||||
func ApplyUserDefaults(u users.User) users.User {
|
||||
func ApplyUserDefaults(u *users.User) {
|
||||
u.StickySidebar = Config.UserDefaults.StickySidebar
|
||||
u.DisableSettings = Config.UserDefaults.DisableSettings
|
||||
u.DarkMode = Config.UserDefaults.DarkMode
|
||||
u.Scope = Config.UserDefaults.Scope
|
||||
u.Locale = Config.UserDefaults.Locale
|
||||
u.ViewMode = Config.UserDefaults.ViewMode
|
||||
u.SingleClick = Config.UserDefaults.SingleClick
|
||||
u.Perm = Config.UserDefaults.Perm
|
||||
u.Sorting = Config.UserDefaults.Sorting
|
||||
u.Commands = Config.UserDefaults.Commands
|
||||
u.ShowHidden = Config.UserDefaults.ShowHidden
|
||||
u.DateFormat = Config.UserDefaults.DateFormat
|
||||
return u
|
||||
u.Scopes = []users.SourceScope{
|
||||
{
|
||||
Scope: Config.Server.DefaultSource.Config.DefaultUserScope,
|
||||
Name: Config.Server.DefaultSource.Path,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,35 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
||||
)
|
||||
|
||||
func TestConfigLoadChanged(t *testing.T) {
|
||||
yamlData, _ := loadConfigFile("./testingConfig.yaml")
|
||||
// Marshal the YAML data to a more human-readable format
|
||||
newConfig := setDefaults()
|
||||
Config := setDefaults()
|
||||
|
||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||
defaultConfig := setDefaults()
|
||||
err := loadConfigWithDefaults("./testingConfig.yaml")
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
t.Fatalf("error loading config file: %v", err)
|
||||
}
|
||||
// Use go-cmp to compare the two structs
|
||||
if diff := cmp.Diff(newConfig, Config); diff == "" {
|
||||
if diff := cmp.Diff(defaultConfig, Config); diff == "" {
|
||||
t.Errorf("No change when there should have been (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLoadSpecificValues(t *testing.T) {
|
||||
yamlData, _ := loadConfigFile("./testingConfig.yaml")
|
||||
// Marshal the YAML data to a more human-readable format
|
||||
newConfig := setDefaults()
|
||||
Config := setDefaults()
|
||||
|
||||
err := yaml.Unmarshal(yamlData, &newConfig)
|
||||
defaultConfig := setDefaults()
|
||||
err := loadConfigWithDefaults("./testingConfig.yaml")
|
||||
if err != nil {
|
||||
logger.Fatal(fmt.Sprintf("Error unmarshaling YAML data: %v", err))
|
||||
t.Fatalf("error loading config file: %v", err)
|
||||
}
|
||||
testCases := []struct {
|
||||
fieldName string
|
||||
globalVal interface{}
|
||||
newVal interface{}
|
||||
}{
|
||||
{"Server.Database", Config.Server.Database, newConfig.Server.Database},
|
||||
{"Server.Database", Config.Server.Database, defaultConfig.Server.Database},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -2,7 +2,6 @@ package settings
|
||||
|
||||
import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/users"
|
||||
)
|
||||
|
||||
// StorageBackend is a settings storage backend.
|
||||
@@ -35,14 +34,6 @@ func (s *Storage) Get() (*Settings, error) {
|
||||
return set, nil
|
||||
}
|
||||
|
||||
var defaultEvents = []string{
|
||||
"save",
|
||||
"copy",
|
||||
"rename",
|
||||
"upload",
|
||||
"delete",
|
||||
}
|
||||
|
||||
// Save saves the settings for the current instance.
|
||||
func (s *Storage) Save(set *Settings) error {
|
||||
if len(set.Auth.Key) == 0 {
|
||||
@@ -53,32 +44,10 @@ func (s *Storage) Save(set *Settings) error {
|
||||
set.UserDefaults.Locale = "en"
|
||||
}
|
||||
|
||||
if set.UserDefaults.Commands == nil {
|
||||
set.UserDefaults.Commands = []string{}
|
||||
}
|
||||
|
||||
if set.UserDefaults.ViewMode == "" {
|
||||
set.UserDefaults.ViewMode = "normal"
|
||||
}
|
||||
|
||||
if set.Rules == nil {
|
||||
set.Rules = []users.Rule{}
|
||||
}
|
||||
|
||||
if set.Commands == nil {
|
||||
set.Commands = map[string][]string{}
|
||||
}
|
||||
|
||||
for _, event := range defaultEvents {
|
||||
if _, ok := set.Commands["before_"+event]; !ok {
|
||||
set.Commands["before_"+event] = []string{}
|
||||
}
|
||||
|
||||
if _, ok := set.Commands["after_"+event]; !ok {
|
||||
set.Commands["after_"+event] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
err := s.back.Save(set)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+31
-29
@@ -15,7 +15,6 @@ const (
|
||||
type Settings struct {
|
||||
Commands map[string][]string `json:"commands"`
|
||||
Shell []string `json:"shell"`
|
||||
Rules []users.Rule `json:"rules"`
|
||||
Server Server `json:"server"`
|
||||
Auth Auth `json:"auth"`
|
||||
Frontend Frontend `json:"frontend"`
|
||||
@@ -35,6 +34,7 @@ type Auth struct {
|
||||
Key []byte `json:"key"`
|
||||
AdminUsername string `json:"adminUsername"`
|
||||
AdminPassword string `json:"adminPassword"`
|
||||
AuthMethods []string
|
||||
}
|
||||
|
||||
type LoginMethods struct {
|
||||
@@ -73,13 +73,16 @@ type Server struct {
|
||||
BaseURL string `json:"baseURL"`
|
||||
Logging []LogConfig `json:"logging"`
|
||||
Database string `json:"database"`
|
||||
Root string `json:"root"`
|
||||
Root string `json:"root"` // deprecated, use sources
|
||||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Sources []Source `json:"sources"`
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
InternalUrl string `json:"internalUrl"` // used by integrations
|
||||
CacheDir string `json:"cacheDir"`
|
||||
// not exposed to config
|
||||
SourceMap map[string]Source // uses realpath as key
|
||||
NameToSource map[string]Source // uses name as key
|
||||
DefaultSource Source
|
||||
}
|
||||
|
||||
type Integrations struct {
|
||||
@@ -102,12 +105,12 @@ type LogConfig struct {
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Config IndexConfig `json:"config"`
|
||||
Path string `json:"path"` // can be relative, filesystem path
|
||||
Name string `json:"name"` // display name
|
||||
Config SourceConfig `json:"config"`
|
||||
}
|
||||
|
||||
type IndexConfig struct {
|
||||
type SourceConfig struct {
|
||||
IndexingInterval uint32 `json:"indexingInterval"`
|
||||
Disabled bool `json:"disabled"`
|
||||
MaxWatchers int `json:"maxWatchers"`
|
||||
@@ -116,6 +119,9 @@ type IndexConfig struct {
|
||||
IgnoreZeroSizeFolders bool `json:"ignoreZeroSizeFolders"`
|
||||
Exclude IndexFilter `json:"exclude"`
|
||||
Include IndexFilter `json:"include"`
|
||||
DefaultUserScope string `json:"defaultUserScope"` // default "" should match folders under path
|
||||
DefaultEnabled bool `json:"defaultEnabled"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
}
|
||||
|
||||
type IndexFilter struct {
|
||||
@@ -142,26 +148,22 @@ type ExternalLink struct {
|
||||
// UserDefaults is a type that holds the default values
|
||||
// for some fields on User.
|
||||
type UserDefaults struct {
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
DisableSettings bool `json:"disableSettings,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Rules []users.Rule `json:"rules"`
|
||||
Sorting struct {
|
||||
By string `json:"by"`
|
||||
Asc bool `json:"asc"`
|
||||
} `json:"sorting"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
ShowHidden bool `json:"showHidden"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
QuickDownload bool `json:"quickDownload"`
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
DisableSettings bool `json:"disableSettings,omitempty"`
|
||||
Scope string `json:"scope"` // deprecated
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
ShowHidden bool `json:"showHidden"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
QuickDownload bool `json:"quickDownload"`
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
|
||||
DefaultScopes []users.SourceScope `json:"-"`
|
||||
}
|
||||
@@ -10,6 +10,7 @@ type CreateBody struct {
|
||||
type Link struct {
|
||||
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"`
|
||||
|
||||
@@ -11,8 +11,8 @@ type StorageBackend interface {
|
||||
All() ([]*Link, error)
|
||||
FindByUserID(id uint) ([]*Link, error)
|
||||
GetByHash(hash string) (*Link, error)
|
||||
GetPermanent(path string, id uint) (*Link, error)
|
||||
Gets(path string, id uint) ([]*Link, error)
|
||||
GetPermanent(path, source string, id uint) (*Link, error)
|
||||
Gets(path, source string, id uint) ([]*Link, error)
|
||||
Save(s *Link) error
|
||||
Delete(hash string) error
|
||||
}
|
||||
@@ -85,13 +85,13 @@ func (s *Storage) GetByHash(hash string) (*Link, error) {
|
||||
}
|
||||
|
||||
// GetPermanent wraps a StorageBackend.GetPermanent
|
||||
func (s *Storage) GetPermanent(path string, id uint) (*Link, error) {
|
||||
return s.back.GetPermanent(path, id)
|
||||
func (s *Storage) GetPermanent(path, source string, id uint) (*Link, error) {
|
||||
return s.back.GetPermanent(path, source, id)
|
||||
}
|
||||
|
||||
// Gets wraps a StorageBackend.Gets
|
||||
func (s *Storage) Gets(path string, id uint) ([]*Link, error) {
|
||||
links, err := s.back.Gets(path, id)
|
||||
func (s *Storage) Gets(path, source string, id uint) ([]*Link, error) {
|
||||
links, err := s.back.Gets(path, source, id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -46,27 +46,26 @@ func (s shareBackend) GetByHash(hash string) (*share.Link, error) {
|
||||
return &v, err
|
||||
}
|
||||
|
||||
func (s shareBackend) GetPermanent(path string, id uint) (*share.Link, error) {
|
||||
func (s shareBackend) GetPermanent(path, source string, id uint) (*share.Link, error) {
|
||||
var v share.Link
|
||||
err := s.db.Select(q.Eq("Path", path), q.Eq("Expire", 0), q.Eq("UserID", id)).First(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return nil, errors.ErrNotExist
|
||||
}
|
||||
|
||||
return &v, err
|
||||
var legacy []*share.Link
|
||||
// todo remove legacy and return notfound errors
|
||||
_ = s.db.Select(q.Eq("Path", path), q.Eq("Source", source), q.Eq("Expire", 0), q.Eq("UserID", id)).First(&v)
|
||||
_ = s.db.Select(q.Eq("Path", path), q.Eq("Source", ""), q.Eq("UserID", id)).Find(&legacy)
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func (s shareBackend) Gets(path string, id uint) ([]*share.Link, error) {
|
||||
func (s shareBackend) Gets(path, source string, id uint) ([]*share.Link, error) {
|
||||
var v []*share.Link
|
||||
err := s.db.Select(q.Eq("Path", path), q.Eq("UserID", id)).Find(&v)
|
||||
if err == storm.ErrNotFound {
|
||||
return v, errors.ErrNotExist
|
||||
}
|
||||
|
||||
var legacy []*share.Link
|
||||
// todo remove legacy and return notfound errors
|
||||
_ = s.db.Select(q.Eq("Path", path), q.Eq("Source", source), q.Eq("UserID", id)).Find(&v)
|
||||
_ = s.db.Select(q.Eq("Path", path), q.Eq("Source", ""), q.Eq("UserID", id)).Find(&legacy)
|
||||
filteredList := []*share.Link{}
|
||||
// automatically delete and clear expired shares
|
||||
var err error
|
||||
// through and filter out expired share
|
||||
for i := range v {
|
||||
if v[i].Expire < time.Now().Unix() {
|
||||
if v[i].Expire < time.Now().Unix() && v[i].Expire != 0 {
|
||||
err = s.Delete(v[i].PasswordHash)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("expired share could not be deleted: %v", err.Error()))
|
||||
@@ -76,6 +75,19 @@ func (s shareBackend) Gets(path string, id uint) ([]*share.Link, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// automatically delete and clear expired shares
|
||||
// todo remove after some time.
|
||||
for i := range legacy {
|
||||
if legacy[i].Expire < time.Now().Unix() && legacy[i].Expire != 0 {
|
||||
err = s.Delete(legacy[i].PasswordHash)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("expired share could not be deleted: %v", err.Error()))
|
||||
}
|
||||
} else {
|
||||
filteredList = append(filteredList, legacy[i])
|
||||
}
|
||||
}
|
||||
|
||||
return filteredList, err
|
||||
}
|
||||
|
||||
|
||||
+117
-18
@@ -3,6 +3,7 @@ package bolt
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
storm "github.com/asdine/storm/v3"
|
||||
|
||||
@@ -58,37 +59,68 @@ func (st usersBackend) Gets() ([]*users.User, error) {
|
||||
return allUsers, err
|
||||
}
|
||||
|
||||
func (st usersBackend) Update(user *users.User, fields ...string) error {
|
||||
if len(fields) == 0 {
|
||||
return st.Save(user)
|
||||
func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...string) error {
|
||||
existingUser, err := st.GetBy(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(user).Elem()
|
||||
fields, err = parseFields(user, fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !actorIsAdmin {
|
||||
err := checkRestrictedFields(existingUser, fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
return fmt.Errorf("no fields to update")
|
||||
}
|
||||
|
||||
// converting scopes to map of paths intead of names (names can change)
|
||||
if slices.Contains(fields, "scopes") || slices.Contains(fields, "Scopes") {
|
||||
adjustedScopes, err := settings.ConvertToBackendScopes(user.Scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Scopes = adjustedScopes
|
||||
}
|
||||
// Use reflection to access struct fields
|
||||
userFields := reflect.ValueOf(user).Elem() // Get struct value
|
||||
|
||||
for _, field := range fields {
|
||||
// Capitalize the first letter (you can adjust this based on your field naming convention)
|
||||
correctedField := utils.CapitalizeFirst(field)
|
||||
|
||||
userField := val.FieldByName(correctedField)
|
||||
if !userField.IsValid() {
|
||||
return fmt.Errorf("invalid field: %s", field)
|
||||
}
|
||||
if !userField.CanSet() {
|
||||
return fmt.Errorf("cannot update unexported field: %s", field)
|
||||
// Get the corresponding field using reflection
|
||||
fieldValue := userFields.FieldByName(correctedField)
|
||||
if !fieldValue.IsValid() {
|
||||
return fmt.Errorf("invalid field: %s", correctedField)
|
||||
}
|
||||
|
||||
val := userField.Interface()
|
||||
// Ensure the field is settable
|
||||
if !fieldValue.CanSet() {
|
||||
return fmt.Errorf("cannot set value of field: %s", correctedField)
|
||||
}
|
||||
|
||||
// Get the value to be stored
|
||||
val := fieldValue.Interface()
|
||||
// Update the database
|
||||
if err := st.db.UpdateField(user, correctedField, val); err != nil {
|
||||
return fmt.Errorf("Error updating user field: %s, error: %v", correctedField, err.Error())
|
||||
return fmt.Errorf("failed to update user field: %s, error: %v", correctedField, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st usersBackend) Save(user *users.User) error {
|
||||
if settings.Config.Auth.Methods.PasswordAuth.Enabled {
|
||||
if len(user.Password) < settings.Config.Auth.Methods.PasswordAuth.MinLength {
|
||||
return fmt.Errorf("password must be at least %d characters long", settings.Config.Auth.Methods.PasswordAuth.MinLength)
|
||||
func (st usersBackend) Save(user *users.User, changePass bool) error {
|
||||
if settings.Config.Auth.Methods.PasswordAuth.Enabled && changePass {
|
||||
err := checkPassword(user.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := users.HashPwd(user.Password)
|
||||
if err != nil {
|
||||
@@ -96,7 +128,15 @@ func (st usersBackend) Save(user *users.User) error {
|
||||
}
|
||||
user.Password = pass
|
||||
}
|
||||
err := st.db.Save(user)
|
||||
|
||||
// converting scopes to map of paths intead of names (names can change)
|
||||
adjustedScopes, err := settings.ConvertToBackendScopes(user.Scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Scopes = adjustedScopes
|
||||
|
||||
err = st.db.Save(user)
|
||||
if err == storm.ErrAlreadyExists {
|
||||
return errors.ErrExist
|
||||
}
|
||||
@@ -115,3 +155,62 @@ func (st usersBackend) DeleteByUsername(username string) error {
|
||||
|
||||
return st.db.DeleteStruct(user)
|
||||
}
|
||||
|
||||
// Define a function to check for restricted fields
|
||||
func checkRestrictedFields(existingUser *users.User, fields []string) error {
|
||||
// Check if 'password' field is locked
|
||||
if existingUser.LockPassword && slices.Contains(fields, "password") {
|
||||
return fmt.Errorf("password is locked")
|
||||
}
|
||||
// Use reflection to get the field names of NonAdminEditable
|
||||
editableFields := reflect.ValueOf(existingUser.NonAdminEditable)
|
||||
if editableFields.Kind() == reflect.Struct {
|
||||
for i := 0; i < editableFields.NumField(); i++ {
|
||||
fieldName := editableFields.Type().Field(i).Name
|
||||
if slices.Contains(fields, fieldName) {
|
||||
return fmt.Errorf("non-admins cannot modify field: %s", fieldName)
|
||||
}
|
||||
}
|
||||
}
|
||||
// No restricted fields found, return nil (no error)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFields(user *users.User, fields []string) ([]string, error) {
|
||||
// If `Which` is not specified, default to updating all fields
|
||||
if len(fields) == 0 || fields[0] == "all" {
|
||||
fields = []string{}
|
||||
v := reflect.ValueOf(user)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
t := v.Type()
|
||||
|
||||
// Dynamically populate fields to update
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if (field.Name == "password" && user.Password != "") || field.Name != "password" {
|
||||
fields = append(fields, field.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if slices.Contains(fields, "password") && user.Password != "" {
|
||||
if settings.Config.Auth.Methods.PasswordAuth.Enabled {
|
||||
pass, err := users.HashPwd(user.Password)
|
||||
if err != nil {
|
||||
return fields, err
|
||||
}
|
||||
user.Password = pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func checkPassword(password string) error {
|
||||
if len(password) < settings.Config.Auth.Methods.PasswordAuth.MinLength {
|
||||
return fmt.Errorf("password must be at least %d characters long", settings.Config.Auth.Methods.PasswordAuth.MinLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+20
-24
@@ -41,7 +41,7 @@ func InitializeDb(path string) (*Storage, bool, error) {
|
||||
return nil, exists, err
|
||||
}
|
||||
|
||||
err = bolt.Save(db, "version", 2) //nolint:gomnd
|
||||
err = bolt.Save(db, "version", 2)
|
||||
if err != nil {
|
||||
return nil, exists, err
|
||||
}
|
||||
@@ -84,49 +84,45 @@ func quickSetup(store *Storage) {
|
||||
utils.CheckErr("store.Settings.Save", err)
|
||||
err = store.Settings.SaveServer(&settings.Config.Server)
|
||||
utils.CheckErr("store.Settings.SaveServer", err)
|
||||
user := settings.ApplyUserDefaults(users.User{})
|
||||
user := &users.User{}
|
||||
settings.ApplyUserDefaults(user)
|
||||
user.Username = settings.Config.Auth.AdminUsername
|
||||
user.Password = settings.Config.Auth.AdminPassword
|
||||
user.Perm.Admin = true
|
||||
user.Scope = "./"
|
||||
user.Scopes = []users.SourceScope{}
|
||||
for _, val := range settings.Config.Server.Sources {
|
||||
user.Scopes = append(user.Scopes, users.SourceScope{
|
||||
Name: val.Path, // backend name is path
|
||||
Scope: "",
|
||||
})
|
||||
}
|
||||
user.DarkMode = true
|
||||
user.ViewMode = "normal"
|
||||
user.LockPassword = false
|
||||
user.Perm = settings.AdminPerms()
|
||||
err = store.Users.Save(&user)
|
||||
logger.Debug(fmt.Sprintf("Creating user as admin: %v %v\n", user.Username, user.Password))
|
||||
err = store.Users.Save(user, true)
|
||||
utils.CheckErr("store.Users.Save", err)
|
||||
}
|
||||
|
||||
// create new user
|
||||
func CreateUser(userInfo users.User, asAdmin bool) error {
|
||||
newUser := &userInfo
|
||||
// must have username or password to create
|
||||
if userInfo.Username == "" || userInfo.Password == "" {
|
||||
return errors.ErrInvalidRequestParams
|
||||
}
|
||||
newUser := settings.ApplyUserDefaults(userInfo)
|
||||
settings.ApplyUserDefaults(newUser)
|
||||
if asAdmin {
|
||||
newUser.Perm = settings.AdminPerms()
|
||||
userInfo.Perm = settings.AdminPerms()
|
||||
}
|
||||
// create new home directory
|
||||
userHome, err := settings.Config.MakeUserDir(newUser.Username, newUser.Scope, files.RootPaths["default"])
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("create user: failed to mkdir user home dir: [%s]", userHome))
|
||||
return err
|
||||
if len(userInfo.Scopes) == 0 {
|
||||
userInfo.Scopes = settings.Config.UserDefaults.DefaultScopes
|
||||
}
|
||||
newUser.Scope = userHome
|
||||
logger.Debug(fmt.Sprintf("user: %s, home dir: [%s].", newUser.Username, userHome))
|
||||
// create new home directories
|
||||
files.MakeUserDirs(newUser)
|
||||
|
||||
// todo: fix this, requries index path to be set
|
||||
idx := files.GetIndex("default")
|
||||
if idx == nil {
|
||||
idx = files.GetIndex("default")
|
||||
}
|
||||
_, _, err = idx.GetRealPath(newUser.Scope)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("user path is not valid: %v", newUser.Scope))
|
||||
return nil
|
||||
}
|
||||
err = store.Users.Save(&newUser)
|
||||
err := store.Users.Save(newUser, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+498
-91
@@ -15,6 +15,228 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/api/createApiKey": {
|
||||
"post": {
|
||||
"description": "Create an API key with specified name, duration, and permissions.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"API Keys"
|
||||
],
|
||||
"summary": "Create API key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name of the API key",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Duration of the API key in days",
|
||||
"name": "days",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Permissions for the API key (comma-separated)",
|
||||
"name": "permissions",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Token created successfully, resoponse contains json object with token key",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.HttpResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/deleteApiKey": {
|
||||
"delete": {
|
||||
"description": "Delete an API key with specified name.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"API Keys"
|
||||
],
|
||||
"summary": "Delete API key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name of the API key to delete",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "API key deleted successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.HttpResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/listApiKeys": {
|
||||
"get": {
|
||||
"description": "List all API keys or retrieve details for a specific key.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"API Keys"
|
||||
],
|
||||
"summary": "List API keys",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name of the API to retrieve details",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of API keys or specific key details",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.AuthTokenMin"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/login": {
|
||||
"post": {
|
||||
"description": "Authenticate a user with a username and password.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "User login",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "JWT token for authentication",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - authentication failed",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/preview": {
|
||||
"get": {
|
||||
"description": "Returns a preview image based on the requested path and size.",
|
||||
@@ -114,7 +336,7 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of specific files within the directory (required)",
|
||||
"description": "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
@@ -140,7 +362,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "Download permissions required",
|
||||
"description": "Modify permissions required",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -178,6 +400,47 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/renew": {
|
||||
"post": {
|
||||
"description": "Refresh the authentication token for a logged-in user.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Renew authentication token",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "New JWT token generated",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - invalid token",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/resources": {
|
||||
"get": {
|
||||
"description": "Returns metadata and optionally file contents for a specified resource path.",
|
||||
@@ -627,7 +890,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/settings": {
|
||||
"get": {
|
||||
"description": "Returns the current configuration settings for signup, user directories, rules, frontend, and commands.",
|
||||
"description": "Returns the current configuration settings for signup, user directories, rules, frontend.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -638,6 +901,14 @@ const docTemplate = `{
|
||||
"Settings"
|
||||
],
|
||||
"summary": "Get system settings",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Property to retrieve",
|
||||
"name": "property",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "System settings data",
|
||||
@@ -648,7 +919,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "Updates the system configuration settings for signup, user directories, rules, frontend, and commands.",
|
||||
"description": "Updates the system configuration settings for signup, user directories, rules, frontend.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -715,6 +986,13 @@ const docTemplate = `{
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for share links",
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -794,6 +1072,20 @@ const docTemplate = `{
|
||||
"schema": {
|
||||
"$ref": "#/definitions/share.CreateBody"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source Path of the files to share",
|
||||
"name": "path",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name of the files to share",
|
||||
"name": "source",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -871,6 +1163,76 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/signup": {
|
||||
"post": {
|
||||
"description": "Register a new user account with a username and password.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "User signup",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User signup details",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.signupBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "User created successfully",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - invalid input",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"405": {
|
||||
"description": "Method not allowed - signup is disabled",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict - user already exists",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/usage": {
|
||||
"get": {
|
||||
"description": "Returns the total and used disk space for a specified directory.",
|
||||
@@ -1164,6 +1526,86 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/public/dl": {
|
||||
"get": {
|
||||
"description": "Returns the raw content of a file, multiple files, or a directory. Supports downloading files as archives in various formats.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Resources"
|
||||
],
|
||||
"summary": "Get raw content of a file, multiple files, or directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'.",
|
||||
"name": "inline",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'.",
|
||||
"name": "algo",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Raw file or directory content, or archive for multiple files",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "Modify permissions required",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request path",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "File or directory not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@@ -1249,6 +1691,26 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.AuthTokenMin": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Permissions": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"expires": {
|
||||
"type": "integer"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.DiskUsageResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1277,15 +1739,6 @@ const docTemplate = `{
|
||||
"http.settingsData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"commands": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"createUserDir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1295,12 +1748,6 @@ const docTemplate = `{
|
||||
"frontend": {
|
||||
"$ref": "#/definitions/settings.Frontend"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.Rule"
|
||||
}
|
||||
},
|
||||
"signup": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1309,6 +1756,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.signupBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ExternalLink": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1388,13 +1846,8 @@ const docTemplate = `{
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.Rule"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"description": "deprecated",
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
@@ -1403,17 +1856,6 @@ const docTemplate = `{
|
||||
"singleClick": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sorting": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"asc": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"by": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stickySidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1454,6 +1896,9 @@ const docTemplate = `{
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token is a random value that will only be set when PasswordHash is set. It is\nURL-Safe and is used to download links in password-protected shares via a\nquery arg.",
|
||||
"type": "string"
|
||||
@@ -1495,57 +1940,14 @@ const docTemplate = `{
|
||||
"api": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"create": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"delete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"download": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"execute": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"modify": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rename": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"share": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.Regexp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"raw": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.Rule": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"regex": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"regexp": {
|
||||
"$ref": "#/definitions/users.Regexp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.Sorting": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1557,6 +1959,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.SourceScope": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1566,12 +1979,6 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/users.AuthToken"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"darkMode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1605,15 +2012,15 @@ const docTemplate = `{
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.Rule"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.SourceScope"
|
||||
}
|
||||
},
|
||||
"showHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,228 @@
|
||||
"contact": {}
|
||||
},
|
||||
"paths": {
|
||||
"/api/createApiKey": {
|
||||
"post": {
|
||||
"description": "Create an API key with specified name, duration, and permissions.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"API Keys"
|
||||
],
|
||||
"summary": "Create API key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name of the API key",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Duration of the API key in days",
|
||||
"name": "days",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Permissions for the API key (comma-separated)",
|
||||
"name": "permissions",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Token created successfully, resoponse contains json object with token key",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.HttpResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/deleteApiKey": {
|
||||
"delete": {
|
||||
"description": "Delete an API key with specified name.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"API Keys"
|
||||
],
|
||||
"summary": "Delete API key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name of the API key to delete",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "API key deleted successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.HttpResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/listApiKeys": {
|
||||
"get": {
|
||||
"description": "List all API keys or retrieve details for a specific key.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"API Keys"
|
||||
],
|
||||
"summary": "List API keys",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name of the API to retrieve details",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of API keys or specific key details",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.AuthTokenMin"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/login": {
|
||||
"post": {
|
||||
"description": "Authenticate a user with a username and password.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "User login",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "JWT token for authentication",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - authentication failed",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/preview": {
|
||||
"get": {
|
||||
"description": "Returns a preview image based on the requested path and size.",
|
||||
@@ -103,7 +325,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of specific files within the directory (required)",
|
||||
"description": "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
@@ -129,7 +351,7 @@
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "Download permissions required",
|
||||
"description": "Modify permissions required",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -167,6 +389,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/renew": {
|
||||
"post": {
|
||||
"description": "Refresh the authentication token for a logged-in user.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Renew authentication token",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "New JWT token generated",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - invalid token",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/resources": {
|
||||
"get": {
|
||||
"description": "Returns metadata and optionally file contents for a specified resource path.",
|
||||
@@ -616,7 +879,7 @@
|
||||
},
|
||||
"/api/settings": {
|
||||
"get": {
|
||||
"description": "Returns the current configuration settings for signup, user directories, rules, frontend, and commands.",
|
||||
"description": "Returns the current configuration settings for signup, user directories, rules, frontend.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -627,6 +890,14 @@
|
||||
"Settings"
|
||||
],
|
||||
"summary": "Get system settings",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Property to retrieve",
|
||||
"name": "property",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "System settings data",
|
||||
@@ -637,7 +908,7 @@
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "Updates the system configuration settings for signup, user directories, rules, frontend, and commands.",
|
||||
"description": "Updates the system configuration settings for signup, user directories, rules, frontend.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -704,6 +975,13 @@
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name for share links",
|
||||
"name": "source",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -783,6 +1061,20 @@
|
||||
"schema": {
|
||||
"$ref": "#/definitions/share.CreateBody"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source Path of the files to share",
|
||||
"name": "path",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Source name of the files to share",
|
||||
"name": "source",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -860,6 +1152,76 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/signup": {
|
||||
"post": {
|
||||
"description": "Register a new user account with a username and password.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "User signup",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "User signup details",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/http.signupBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "User created successfully",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - invalid input",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"405": {
|
||||
"description": "Method not allowed - signup is disabled",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict - user already exists",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/usage": {
|
||||
"get": {
|
||||
"description": "Returns the total and used disk space for a specified directory.",
|
||||
@@ -1153,6 +1515,86 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/public/dl": {
|
||||
"get": {
|
||||
"description": "Returns the raw content of a file, multiple files, or a directory. Supports downloading files as archives in various formats.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Resources"
|
||||
],
|
||||
"summary": "Get raw content of a file, multiple files, or directory",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "a list of files in the following format 'source::filename' and separated by '||' with additional items in the list. (required)",
|
||||
"name": "files",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults to 'attachment'.",
|
||||
"name": "inline",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Compression algorithm for archiving multiple files or directories. Options: 'zip' and 'tar.gz'. Default is 'zip'.",
|
||||
"name": "algo",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Raw file or directory content, or archive for multiple files",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "Modify permissions required",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request path",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "File or directory not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@@ -1238,6 +1680,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.AuthTokenMin": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Permissions": {
|
||||
"$ref": "#/definitions/users.Permissions"
|
||||
},
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"expires": {
|
||||
"type": "integer"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.DiskUsageResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1266,15 +1728,6 @@
|
||||
"http.settingsData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"commands": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"createUserDir": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1284,12 +1737,6 @@
|
||||
"frontend": {
|
||||
"$ref": "#/definitions/settings.Frontend"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.Rule"
|
||||
}
|
||||
},
|
||||
"signup": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1298,6 +1745,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"http.signupBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.ExternalLink": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1377,13 +1835,8 @@
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.Rule"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"description": "deprecated",
|
||||
"type": "string"
|
||||
},
|
||||
"showHidden": {
|
||||
@@ -1392,17 +1845,6 @@
|
||||
"singleClick": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sorting": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"asc": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"by": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stickySidebar": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1443,6 +1885,9 @@
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token is a random value that will only be set when PasswordHash is set. It is\nURL-Safe and is used to download links in password-protected shares via a\nquery arg.",
|
||||
"type": "string"
|
||||
@@ -1484,57 +1929,14 @@
|
||||
"api": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"create": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"delete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"download": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"execute": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"modify": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rename": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"share": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.Regexp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"raw": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.Rule": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"regex": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"regexp": {
|
||||
"$ref": "#/definitions/users.Regexp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.Sorting": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1546,6 +1948,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.SourceScope": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1555,12 +1968,6 @@
|
||||
"$ref": "#/definitions/users.AuthToken"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"darkMode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1594,15 +2001,15 @@
|
||||
"quickDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.Rule"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.SourceScope"
|
||||
}
|
||||
},
|
||||
"showHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -57,6 +57,19 @@ definitions:
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
http.AuthTokenMin:
|
||||
properties:
|
||||
Permissions:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
created:
|
||||
type: integer
|
||||
expires:
|
||||
type: integer
|
||||
key:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
http.DiskUsageResponse:
|
||||
properties:
|
||||
total:
|
||||
@@ -75,27 +88,24 @@ definitions:
|
||||
type: object
|
||||
http.settingsData:
|
||||
properties:
|
||||
commands:
|
||||
additionalProperties:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
createUserDir:
|
||||
type: boolean
|
||||
defaults:
|
||||
$ref: '#/definitions/settings.UserDefaults'
|
||||
frontend:
|
||||
$ref: '#/definitions/settings.Frontend'
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/definitions/users.Rule'
|
||||
type: array
|
||||
signup:
|
||||
type: boolean
|
||||
userHomeBasePath:
|
||||
type: string
|
||||
type: object
|
||||
http.signupBody:
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
settings.ExternalLink:
|
||||
properties:
|
||||
text:
|
||||
@@ -148,23 +158,13 @@ definitions:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
quickDownload:
|
||||
type: boolean
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/definitions/users.Rule'
|
||||
type: array
|
||||
scope:
|
||||
description: deprecated
|
||||
type: string
|
||||
showHidden:
|
||||
type: boolean
|
||||
singleClick:
|
||||
type: boolean
|
||||
sorting:
|
||||
properties:
|
||||
asc:
|
||||
type: boolean
|
||||
by:
|
||||
type: string
|
||||
type: object
|
||||
stickySidebar:
|
||||
type: boolean
|
||||
themeColor:
|
||||
@@ -191,6 +191,8 @@ definitions:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
token:
|
||||
description: |-
|
||||
Token is a random value that will only be set when PasswordHash is set. It is
|
||||
@@ -221,39 +223,11 @@ definitions:
|
||||
type: boolean
|
||||
api:
|
||||
type: boolean
|
||||
create:
|
||||
type: boolean
|
||||
delete:
|
||||
type: boolean
|
||||
download:
|
||||
type: boolean
|
||||
execute:
|
||||
type: boolean
|
||||
modify:
|
||||
type: boolean
|
||||
rename:
|
||||
type: boolean
|
||||
share:
|
||||
type: boolean
|
||||
type: object
|
||||
users.Regexp:
|
||||
properties:
|
||||
raw:
|
||||
type: string
|
||||
type: object
|
||||
users.Rule:
|
||||
properties:
|
||||
allow:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
regex:
|
||||
type: boolean
|
||||
regexp:
|
||||
$ref: '#/definitions/users.Regexp'
|
||||
type: object
|
||||
users.Sorting:
|
||||
properties:
|
||||
asc:
|
||||
@@ -261,16 +235,19 @@ definitions:
|
||||
by:
|
||||
type: string
|
||||
type: object
|
||||
users.SourceScope:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
scope:
|
||||
type: string
|
||||
type: object
|
||||
users.User:
|
||||
properties:
|
||||
apiKeys:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/users.AuthToken'
|
||||
type: object
|
||||
commands:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
darkMode:
|
||||
type: boolean
|
||||
dateFormat:
|
||||
@@ -293,12 +270,12 @@ definitions:
|
||||
$ref: '#/definitions/users.Permissions'
|
||||
quickDownload:
|
||||
type: boolean
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/definitions/users.Rule'
|
||||
type: array
|
||||
scope:
|
||||
type: string
|
||||
scopes:
|
||||
items:
|
||||
$ref: '#/definitions/users.SourceScope'
|
||||
type: array
|
||||
showHidden:
|
||||
type: boolean
|
||||
singleClick:
|
||||
@@ -317,6 +294,154 @@ definitions:
|
||||
info:
|
||||
contact: {}
|
||||
paths:
|
||||
/api/createApiKey:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create an API key with specified name, duration, and permissions.
|
||||
parameters:
|
||||
- description: Name of the API key
|
||||
in: query
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: Duration of the API key in days
|
||||
in: query
|
||||
name: days
|
||||
required: true
|
||||
type: string
|
||||
- description: Permissions for the API key (comma-separated)
|
||||
in: query
|
||||
name: permissions
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Token created successfully, resoponse contains json object
|
||||
with token key
|
||||
schema:
|
||||
$ref: '#/definitions/http.HttpResponse'
|
||||
"400":
|
||||
description: Bad request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"404":
|
||||
description: Not found
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"409":
|
||||
description: Conflict
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Create API key
|
||||
tags:
|
||||
- API Keys
|
||||
/api/deleteApiKey:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete an API key with specified name.
|
||||
parameters:
|
||||
- description: Name of the API key to delete
|
||||
in: query
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: API key deleted successfully
|
||||
schema:
|
||||
$ref: '#/definitions/http.HttpResponse'
|
||||
"404":
|
||||
description: Not found
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Delete API key
|
||||
tags:
|
||||
- API Keys
|
||||
/api/listApiKeys:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: List all API keys or retrieve details for a specific key.
|
||||
parameters:
|
||||
- description: Name of the API to retrieve details
|
||||
in: query
|
||||
name: name
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: List of API keys or specific key details
|
||||
schema:
|
||||
$ref: '#/definitions/http.AuthTokenMin'
|
||||
"404":
|
||||
description: Not found
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: List API keys
|
||||
tags:
|
||||
- API Keys
|
||||
/api/login:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Authenticate a user with a username and password.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: JWT token for authentication
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: Forbidden - authentication failed
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: User login
|
||||
tags:
|
||||
- Auth
|
||||
/api/preview:
|
||||
get:
|
||||
consumes:
|
||||
@@ -380,7 +505,8 @@ paths:
|
||||
description: Returns the raw content of a file, multiple files, or a directory.
|
||||
Supports downloading files as archives in various formats.
|
||||
parameters:
|
||||
- description: Comma-separated list of specific files within the directory (required)
|
||||
- description: a list of files in the following format 'source::filename' and
|
||||
separated by '||' with additional items in the list. (required)
|
||||
in: query
|
||||
name: files
|
||||
required: true
|
||||
@@ -403,7 +529,7 @@ paths:
|
||||
schema:
|
||||
type: file
|
||||
"202":
|
||||
description: Download permissions required
|
||||
description: Modify permissions required
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
@@ -429,6 +555,33 @@ paths:
|
||||
summary: Get raw content of a file, multiple files, or directory
|
||||
tags:
|
||||
- Resources
|
||||
/api/renew:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Refresh the authentication token for a logged-in user.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: New JWT token generated
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized - invalid token
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Renew authentication token
|
||||
tags:
|
||||
- Auth
|
||||
/api/resources:
|
||||
delete:
|
||||
consumes:
|
||||
@@ -736,7 +889,12 @@ paths:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns the current configuration settings for signup, user directories,
|
||||
rules, frontend, and commands.
|
||||
rules, frontend.
|
||||
parameters:
|
||||
- description: Property to retrieve
|
||||
in: query
|
||||
name: property
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -751,7 +909,7 @@ paths:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Updates the system configuration settings for signup, user directories,
|
||||
rules, frontend, and commands.
|
||||
rules, frontend.
|
||||
parameters:
|
||||
- description: Settings data to update
|
||||
in: body
|
||||
@@ -791,6 +949,11 @@ paths:
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name for share links
|
||||
in: query
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -845,6 +1008,16 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/share.CreateBody'
|
||||
- description: Source Path of the files to share
|
||||
in: path
|
||||
name: path
|
||||
required: true
|
||||
type: string
|
||||
- description: Source name of the files to share
|
||||
in: path
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -898,6 +1071,52 @@ paths:
|
||||
summary: Delete a share link
|
||||
tags:
|
||||
- Shares
|
||||
/api/signup:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Register a new user account with a username and password.
|
||||
parameters:
|
||||
- description: User signup details
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/http.signupBody'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: User created successfully
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Bad request - invalid input
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"405":
|
||||
description: Method not allowed - signup is disabled
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"409":
|
||||
description: Conflict - user already exists
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: User signup
|
||||
tags:
|
||||
- Auth
|
||||
/api/usage:
|
||||
get:
|
||||
consumes:
|
||||
@@ -1092,4 +1311,61 @@ paths:
|
||||
summary: Health Check
|
||||
tags:
|
||||
- Health
|
||||
/public/dl:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
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 'source::filename' and
|
||||
separated by '||' with additional items in the list. (required)
|
||||
in: query
|
||||
name: files
|
||||
required: true
|
||||
type: string
|
||||
- description: If true, sets 'Content-Disposition' to 'inline'. Otherwise, defaults
|
||||
to 'attachment'.
|
||||
in: query
|
||||
name: inline
|
||||
type: boolean
|
||||
- description: 'Compression algorithm for archiving multiple files or directories.
|
||||
Options: ''zip'' and ''tar.gz''. Default is ''zip''.'
|
||||
in: query
|
||||
name: algo
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Raw file or directory content, or archive for multiple files
|
||||
schema:
|
||||
type: file
|
||||
"202":
|
||||
description: Modify permissions required
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid request path
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"404":
|
||||
description: File or directory not found
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get raw content of a file, multiple files, or directory
|
||||
tags:
|
||||
- Resources
|
||||
swagger: "2.0"
|
||||
@@ -11,7 +11,6 @@ func HashPwd(password string) (string, error) {
|
||||
}
|
||||
|
||||
// CheckPwd checks if a password is correct.
|
||||
func CheckPwd(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
func CheckPwd(password, hash string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Checker is a Rules checker.
|
||||
type Checker interface {
|
||||
Check(path string) bool
|
||||
}
|
||||
|
||||
// Check implements rules.Checker.
|
||||
func (user *User) Check(path string) bool {
|
||||
allow := true
|
||||
for _, rule := range user.Rules {
|
||||
if rule.Matches(path) {
|
||||
allow = rule.Allow
|
||||
}
|
||||
}
|
||||
|
||||
return allow
|
||||
}
|
||||
|
||||
// Rule is a allow/disallow rule.
|
||||
type Rule struct {
|
||||
Regex bool `json:"regex"`
|
||||
Allow bool `json:"allow"`
|
||||
Path string `json:"path"`
|
||||
Regexp *Regexp `json:"regexp"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// MatchHidden matches paths with a basename
|
||||
// that begins with a dot.
|
||||
func MatchHidden(path string) bool {
|
||||
return path != "" && strings.HasPrefix(filepath.Base(path), ".")
|
||||
}
|
||||
|
||||
// Matches matches a path against a rule.
|
||||
func (r *Rule) Matches(path string) bool {
|
||||
if r.Regex {
|
||||
return r.Regexp.MatchString(path)
|
||||
}
|
||||
|
||||
return strings.HasPrefix(path, r.Path)
|
||||
}
|
||||
|
||||
// Regexp is a wrapper to the native regexp type where we
|
||||
// save the raw expression.
|
||||
type Regexp struct {
|
||||
Raw string `json:"raw"`
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
// MatchString checks if a string matches the regexp.
|
||||
func (r *Regexp) MatchString(s string) bool {
|
||||
if r.regexp == nil {
|
||||
r.regexp = regexp.MustCompile(r.Raw)
|
||||
}
|
||||
|
||||
return r.regexp.MatchString(s)
|
||||
}
|
||||
+16
-60
@@ -11,24 +11,22 @@ import (
|
||||
type StorageBackend interface {
|
||||
GetBy(interface{}) (*User, error)
|
||||
Gets() ([]*User, error)
|
||||
Save(u *User) error
|
||||
Update(u *User, fields ...string) error
|
||||
Save(u *User, changePass bool) error
|
||||
Update(u *User, adminActor bool, fields ...string) error
|
||||
DeleteByID(uint) error
|
||||
DeleteByUsername(string) error
|
||||
}
|
||||
|
||||
// Store is an interface for user storage.
|
||||
type Store interface {
|
||||
Get(baseScope string, id interface{}) (user *User, err error)
|
||||
Gets(baseScope string) ([]*User, error)
|
||||
Update(user *User, fields ...string) error
|
||||
Save(user *User) error
|
||||
Get(id interface{}) (user *User, err error)
|
||||
Gets() ([]*User, error)
|
||||
Update(user *User, adminActor bool, fields ...string) error
|
||||
Save(user *User, changePass bool) error
|
||||
Delete(id interface{}) error
|
||||
LastUpdate(id uint) int64
|
||||
AddApiKey(username uint, name string, key AuthToken) error
|
||||
DeleteApiKey(username uint, name string) error
|
||||
AddRule(username string, rule Rule) error
|
||||
DeleteRule(username string, ruleID string) error
|
||||
}
|
||||
|
||||
// Storage is a users storage.
|
||||
@@ -49,7 +47,7 @@ func NewStorage(back StorageBackend) *Storage {
|
||||
// Get allows you to get a user by its name or username. The provided
|
||||
// id must be a string for username lookup or a uint for id lookup. If id
|
||||
// is neither, a ErrInvalidDataType will be returned.
|
||||
func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error) {
|
||||
func (s *Storage) Get(id interface{}) (user *User, err error) {
|
||||
user, err = s.back.GetBy(id)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -58,7 +56,7 @@ func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error)
|
||||
}
|
||||
|
||||
// Gets gets a list of all users.
|
||||
func (s *Storage) Gets(baseScope string) ([]*User, error) {
|
||||
func (s *Storage) Gets() ([]*User, error) {
|
||||
users, err := s.back.Gets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -67,8 +65,8 @@ func (s *Storage) Gets(baseScope string) ([]*User, error) {
|
||||
}
|
||||
|
||||
// Update updates a user in the database.
|
||||
func (s *Storage) Update(user *User, fields ...string) error {
|
||||
err := s.back.Update(user, fields...)
|
||||
func (s *Storage) Update(user *User, adminIActor bool, fields ...string) error {
|
||||
err := s.back.Update(user, adminIActor, fields...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,25 +77,8 @@ func (s *Storage) Update(user *User, fields ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRule adds a rule to the user's rules list and updates the user in the database.
|
||||
func (s *Storage) AddRule(userID string, rule Rule) error {
|
||||
user, err := s.Get("", userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Rules = append(user.Rules, rule)
|
||||
|
||||
err = s.Update(user, "Rules")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) AddApiKey(userID uint, name string, key AuthToken) error {
|
||||
user, err := s.Get("", userID)
|
||||
user, err := s.Get(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -106,7 +87,7 @@ func (s *Storage) AddApiKey(userID uint, name string, key AuthToken) error {
|
||||
user.ApiKeys = make(map[string]AuthToken)
|
||||
}
|
||||
user.ApiKeys[name] = key
|
||||
err = s.Update(user, "ApiKeys")
|
||||
err = s.Update(user, false, "ApiKeys")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -115,7 +96,7 @@ func (s *Storage) AddApiKey(userID uint, name string, key AuthToken) error {
|
||||
}
|
||||
|
||||
func (s *Storage) DeleteApiKey(userID uint, name string) error {
|
||||
user, err := s.Get("", userID)
|
||||
user, err := s.Get(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,32 +105,7 @@ func (s *Storage) DeleteApiKey(userID uint, name string) error {
|
||||
user.ApiKeys = make(map[string]AuthToken)
|
||||
}
|
||||
delete(user.ApiKeys, name)
|
||||
err = s.Update(user, "ApiKeys")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRule deletes a rule specified by ID from the user's rules list and updates the user in the database.
|
||||
func (s *Storage) DeleteRule(userID string, ruleID string) error {
|
||||
user, err := s.Get("", userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find and remove the rule with the specified ID
|
||||
var updatedRules []Rule
|
||||
for _, r := range user.Rules {
|
||||
if r.Id != ruleID {
|
||||
updatedRules = append(updatedRules, r)
|
||||
}
|
||||
}
|
||||
|
||||
user.Rules = updatedRules
|
||||
|
||||
err = s.Update(user, "Rules")
|
||||
err = s.Update(user, false, "ApiKeys")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,8 +114,8 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
|
||||
}
|
||||
|
||||
// Save saves the user in a storage.
|
||||
func (s *Storage) Save(user *User) error {
|
||||
return s.back.Save(user)
|
||||
func (s *Storage) Save(user *User, changePass bool) error {
|
||||
return s.back.Save(user, changePass)
|
||||
}
|
||||
|
||||
// Delete allows you to delete a user by its name or username. The provided
|
||||
|
||||
+42
-64
@@ -1,8 +1,6 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
@@ -17,15 +15,10 @@ type AuthToken struct {
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
Api bool `json:"api"`
|
||||
Admin bool `json:"admin"`
|
||||
Execute bool `json:"execute"`
|
||||
Create bool `json:"create"`
|
||||
Rename bool `json:"rename"`
|
||||
Modify bool `json:"modify"`
|
||||
Delete bool `json:"delete"`
|
||||
Share bool `json:"share"`
|
||||
Download bool `json:"download"`
|
||||
Api bool `json:"api"`
|
||||
Admin bool `json:"admin"`
|
||||
Modify bool `json:"modify"`
|
||||
Share bool `json:"share"`
|
||||
}
|
||||
|
||||
// SortingSettings represents the sorting settings.
|
||||
@@ -36,64 +29,49 @@ type Sorting struct {
|
||||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DarkMode bool `json:"darkMode"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Rules []Rule `json:"rules"`
|
||||
ApiKeys map[string]AuthToken `json:"apiKeys,omitempty"`
|
||||
ShowHidden bool `json:"showHidden"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
QuickDownload bool `json:"quickDownload"`
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
|
||||
NonAdminEditable
|
||||
StickySidebar bool `json:"stickySidebar"`
|
||||
DisableSettings bool `json:"disableSettings"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Scopes []SourceScope `json:"scopes"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
Perm Permissions `json:"perm"`
|
||||
ApiKeys map[string]AuthToken `json:"apiKeys,omitempty"`
|
||||
}
|
||||
|
||||
type SourceScope struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type NonAdminEditable struct {
|
||||
DarkMode bool `json:"darkMode"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode string `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Sorting Sorting `json:"sorting"`
|
||||
ShowHidden bool `json:"showHidden"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
GallerySize int `json:"gallerySize"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
QuickDownload bool `json:"quickDownload"`
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"`
|
||||
}
|
||||
|
||||
var PublicUser = User{
|
||||
NonAdminEditable: NonAdminEditable{
|
||||
Password: "publicUser", // temp user not registered
|
||||
ViewMode: "normal",
|
||||
},
|
||||
Username: "publicUser", // temp user not registered
|
||||
Password: "publicUser", // temp user not registered
|
||||
Scope: "/does/not/exist",
|
||||
ViewMode: "normal",
|
||||
LockPassword: true,
|
||||
Perm: Permissions{
|
||||
Create: false,
|
||||
Rename: false,
|
||||
Modify: false,
|
||||
Delete: false,
|
||||
Share: false,
|
||||
Download: true,
|
||||
Admin: false,
|
||||
Api: false,
|
||||
Modify: false,
|
||||
Share: false,
|
||||
Admin: false,
|
||||
Api: false,
|
||||
},
|
||||
}
|
||||
|
||||
// GetRules implements rules.Provider.
|
||||
func (u *User) GetRules() []Rule {
|
||||
return u.Rules
|
||||
}
|
||||
|
||||
// CanExecute checks if an user can execute a specific command.
|
||||
func (u *User) CanExecute(command string) bool {
|
||||
if !u.Perm.Execute {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, cmd := range u.Commands {
|
||||
if regexp.MustCompile(cmd).MatchString(command) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -92,3 +92,15 @@ func HashSHA256(data string) string {
|
||||
bytes := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(bytes[:])
|
||||
}
|
||||
|
||||
func GetLastComponent(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
path = strings.TrimSuffix(path, "/") // Remove trailing slash if any
|
||||
lastSlash := strings.LastIndex(path, "/")
|
||||
if lastSlash == -1 {
|
||||
return path // No parent directory for a relative path without slashes
|
||||
}
|
||||
return path[lastSlash+1:]
|
||||
}
|
||||
@@ -45,9 +45,9 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-vue": "^9.24.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"vite": "^5.2.7",
|
||||
"vite": "^6.2.0",
|
||||
"vite-plugin-compression2": "^1.0.0",
|
||||
"vitest": "^2.1.5",
|
||||
"vitest": "^3.0.7",
|
||||
"vue-tsc": "^2.0.7"
|
||||
}
|
||||
}
|
||||
+46
-40
@@ -1,5 +1,5 @@
|
||||
import { fetchURL, adjustedData } from './utils'
|
||||
import { removePrefix, getApiPath } from '@/utils/url.js'
|
||||
import { getApiPath, extractSourceFromPath } from '@/utils/url.js'
|
||||
import { state } from '@/store'
|
||||
import { notify } from '@/notify'
|
||||
import { externalUrl } from '@/utils/constants'
|
||||
@@ -7,10 +7,11 @@ import { externalUrl } from '@/utils/constants'
|
||||
// Notify if errors occur
|
||||
export async function fetchFiles (url, content = false) {
|
||||
try {
|
||||
let path = encodeURIComponent(removePrefix(url, 'files'))
|
||||
const result = extractSourceFromPath(url)
|
||||
const apiPath = getApiPath('api/resources', {
|
||||
path: path,
|
||||
content: content
|
||||
path: encodeURIComponent(result.path),
|
||||
source: result.source,
|
||||
...(content && { content: 'true' })
|
||||
})
|
||||
const res = await fetchURL(apiPath)
|
||||
const data = await res.json()
|
||||
@@ -24,12 +25,15 @@ export async function fetchFiles (url, content = false) {
|
||||
|
||||
async function resourceAction (url, method, content) {
|
||||
try {
|
||||
const result = extractSourceFromPath(url)
|
||||
let source = result.source
|
||||
let path = result.path
|
||||
let opts = { method }
|
||||
if (content) {
|
||||
opts.body = content
|
||||
}
|
||||
let path = encodeURIComponent(removePrefix(url, 'files'))
|
||||
const apiPath = getApiPath('api/resources', { path: path })
|
||||
path = encodeURIComponent(path)
|
||||
const apiPath = getApiPath('api/resources', { path: path, source: source })
|
||||
const res = await fetchURL(apiPath, opts)
|
||||
return res
|
||||
} catch (err) {
|
||||
@@ -40,8 +44,7 @@ async function resourceAction (url, method, content) {
|
||||
|
||||
export async function remove (url) {
|
||||
try {
|
||||
let path = encodeURIComponent(removePrefix(url, 'files'))
|
||||
return await resourceAction(path, 'DELETE')
|
||||
return await resourceAction(url, 'DELETE')
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error deleting resource')
|
||||
throw err
|
||||
@@ -50,8 +53,7 @@ export async function remove (url) {
|
||||
|
||||
export async function put (url, content = '') {
|
||||
try {
|
||||
let path = encodeURIComponent(removePrefix(url, 'files'))
|
||||
return await resourceAction(path, 'PUT', content)
|
||||
return await resourceAction(url, 'PUT', content)
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error putting resource')
|
||||
throw err
|
||||
@@ -59,25 +61,34 @@ export async function put (url, content = '') {
|
||||
}
|
||||
|
||||
export function download (format, files) {
|
||||
if (format != 'zip') {
|
||||
if (format !== 'zip') {
|
||||
format = 'tar.gz'
|
||||
}
|
||||
try {
|
||||
let fileargs = ''
|
||||
if (files.length === 1) {
|
||||
fileargs = decodeURI(removePrefix(files[0], 'files'))
|
||||
const result = extractSourceFromPath(decodeURI(files[0]))
|
||||
fileargs = result.source + '::' + result.path + '||'
|
||||
} else {
|
||||
for (let file of files) {
|
||||
fileargs += decodeURI(removePrefix(file, 'files')) + ',|'
|
||||
const result = extractSourceFromPath(decodeURI(file))
|
||||
fileargs += result.source + '::' + result.path + '||'
|
||||
}
|
||||
fileargs = fileargs.substring(0, fileargs.length - 1)
|
||||
}
|
||||
fileargs = fileargs.slice(0, -2) // remove trailing "||"
|
||||
const apiPath = getApiPath('api/raw', {
|
||||
files: encodeURIComponent(fileargs),
|
||||
algo: format
|
||||
})
|
||||
const url = window.origin + apiPath
|
||||
window.open(url)
|
||||
|
||||
// Create a temporary <a> element to trigger the download
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', '') // Ensures it triggers a download
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link) // Clean up
|
||||
} catch (err) {
|
||||
notify.showError(err.message || 'Error downloading files')
|
||||
}
|
||||
@@ -85,8 +96,7 @@ export function download (format, files) {
|
||||
|
||||
export async function post (url, content = '', overwrite = false, onupload) {
|
||||
try {
|
||||
url = removePrefix(url, 'files')
|
||||
|
||||
const result = extractSourceFromPath(url)
|
||||
let bufferContent
|
||||
if (
|
||||
content instanceof Blob &&
|
||||
@@ -96,7 +106,8 @@ export async function post (url, content = '', overwrite = false, onupload) {
|
||||
}
|
||||
|
||||
const apiPath = getApiPath('api/resources', {
|
||||
path: url,
|
||||
path: result.path,
|
||||
source: result.source,
|
||||
override: overwrite
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -146,15 +157,17 @@ export async function moveCopy (
|
||||
try {
|
||||
// Create an array of fetch calls
|
||||
let promises = items.map(item => {
|
||||
let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), 'files'))
|
||||
let fromPath = encodeURIComponent(
|
||||
removePrefix(decodeURI(item.from), 'files')
|
||||
)
|
||||
let localParams = { ...params, destination: toPath, from: fromPath }
|
||||
const fromResult = extractSourceFromPath(item.from)
|
||||
const toResult = extractSourceFromPath(item.to)
|
||||
let localParams = {
|
||||
...params,
|
||||
destination: toResult.source + '::' + toResult.path,
|
||||
from: fromResult.source + '::' + fromResult.path
|
||||
}
|
||||
const apiPath = getApiPath('api/resources', localParams)
|
||||
|
||||
return fetch(apiPath, { method: 'PATCH' }).then(response => {
|
||||
if (!response.ok) {
|
||||
// Throw an error if the fetch fails
|
||||
return response.text().then(text => {
|
||||
throw new Error(
|
||||
`Failed to move/copy: ${text || response.statusText}`
|
||||
@@ -173,10 +186,12 @@ export async function moveCopy (
|
||||
}
|
||||
}
|
||||
|
||||
export async function checksum (path, algo) {
|
||||
export async function checksum (url, algo) {
|
||||
try {
|
||||
const result = extractSourceFromPath(url)
|
||||
const params = {
|
||||
path: encodeURIComponent(removePrefix(path, 'files')),
|
||||
path: encodeURIComponent(result.path),
|
||||
source: result.source,
|
||||
checksum: algo
|
||||
}
|
||||
const apiPath = getApiPath('api/resources', params)
|
||||
@@ -189,10 +204,10 @@ export async function checksum (path, algo) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getDownloadURL (path, inline, useExternal) {
|
||||
export function getDownloadURL (source, path, inline, useExternal) {
|
||||
try {
|
||||
const params = {
|
||||
files: encodeURIComponent(removePrefix(decodeURI(path), 'files')),
|
||||
files: source + '::' + encodeURIComponent(path),
|
||||
...(inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
@@ -206,12 +221,13 @@ export function getDownloadURL (path, inline, useExternal) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreviewURL (path, size, modified) {
|
||||
export function getPreviewURL (source, path, size, modified) {
|
||||
try {
|
||||
const params = {
|
||||
path: encodeURIComponent(removePrefix(decodeURI(path), 'files')),
|
||||
path: encodeURIComponent(path),
|
||||
size: size,
|
||||
key: Date.parse(modified),
|
||||
source: source,
|
||||
inline: 'true'
|
||||
}
|
||||
const apiPath = getApiPath('api/preview', params)
|
||||
@@ -222,16 +238,6 @@ export function getPreviewURL (path, size, modified) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getSubtitlesURL (path) {
|
||||
const params = {
|
||||
inline: true,
|
||||
files: path,
|
||||
source: 'default'
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
return window.origin + apiPath
|
||||
}
|
||||
|
||||
export async function usage (source) {
|
||||
try {
|
||||
const apiPath = getApiPath('api/usage', { source: source })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { adjustedData } from "./utils";
|
||||
import { getApiPath, removePrefix } from "@/utils/url.js";
|
||||
import { getApiPath, removePrefix,extractSourceFromPath } from "@/utils/url.js";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
// Fetch public share data
|
||||
@@ -25,16 +25,23 @@ export async function fetchPub(path, hash, password = "") {
|
||||
// Download files with given parameters
|
||||
export function download(share, ...files) {
|
||||
try {
|
||||
let fileInfo = files[0]
|
||||
if (files.length > 1) {
|
||||
fileInfo = files.map(encodeURIComponent).join(",");
|
||||
let fileargs = ''
|
||||
if (files.length === 1) {
|
||||
const result = extractSourceFromPath(decodeURI(files[0]))
|
||||
fileargs = result.path + '||'
|
||||
} else {
|
||||
for (let file of files) {
|
||||
const result = extractSourceFromPath(decodeURI(file))
|
||||
fileargs += result.path + '||'
|
||||
}
|
||||
}
|
||||
fileargs = fileargs.slice(0, -2); // remove trailing "||"
|
||||
const params = {
|
||||
"path": removePrefix(share.path, "share"),
|
||||
"files": fileargs,
|
||||
"hash": share.hash,
|
||||
"token": share.token,
|
||||
"inline": share.inline,
|
||||
"files": fileInfo,
|
||||
};
|
||||
const apiPath = getApiPath("api/public/dl", params);
|
||||
window.open(window.origin+apiPath)
|
||||
@@ -57,7 +64,14 @@ export async function getPublicUser() {
|
||||
}
|
||||
|
||||
// Generate a download URL
|
||||
export function getDownloadURL(share) {
|
||||
const apiPath = getApiPath("api/public/dl", share);
|
||||
export function getDownloadURL(share,files) {
|
||||
const params = {
|
||||
path: share.path,
|
||||
files: files,
|
||||
hash: share.hash,
|
||||
token: share.token,
|
||||
...(share.inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getApiPath("api/public/dl", params);
|
||||
return window.origin+apiPath
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { fetchURL } from "./utils";
|
||||
import { notify } from "@/notify"; // Import notify for error handling
|
||||
import { removePrefix, getApiPath } from "@/utils/url.js";
|
||||
|
||||
export default async function search(base, query) {
|
||||
export default async function search(base, source, query) {
|
||||
try {
|
||||
base = removePrefix(base,"files");
|
||||
query = encodeURIComponent(query);
|
||||
@@ -11,7 +11,7 @@ export default async function search(base, query) {
|
||||
base += "/";
|
||||
}
|
||||
|
||||
const apiPath = getApiPath("api/search", { scope: base, query: query });
|
||||
const apiPath = getApiPath("api/search", { scope: base, query: query, source: source });
|
||||
const res = await fetchURL(apiPath);
|
||||
let data = await res.json();
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { fetchURL, fetchJSON } from "./utils";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
|
||||
const apiPath = getApiPath("api/settings");
|
||||
|
||||
export function get() {
|
||||
return fetchJSON(apiPath);
|
||||
export function get(property="") {
|
||||
const path = getApiPath("api/settings", { property });
|
||||
return fetchJSON(path);
|
||||
}
|
||||
|
||||
export async function update(settings) {
|
||||
|
||||
await fetchURL(apiPath, {
|
||||
await fetchURL("api/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
@@ -28,8 +28,8 @@ export async function remove(hash) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function create(path, password = "", expires = "", unit = "hours") {
|
||||
const params = { path };
|
||||
export async function create(path, source, password = "", expires = "", unit = "hours") {
|
||||
const params = { path: path, source: source };
|
||||
const apiPath = getApiPath("api/share",params);
|
||||
let body = "{}";
|
||||
if (password != "" || expires !== "" || unit !== "hours") {
|
||||
|
||||
@@ -10,14 +10,9 @@ export async function fetchURL(url, opts, auth = true) {
|
||||
|
||||
let res;
|
||||
try {
|
||||
let userScope = "";
|
||||
if (state.user) {
|
||||
userScope = state.user.scope;
|
||||
}
|
||||
res = await fetch(url, {
|
||||
headers: {
|
||||
"sessionId": state.sessionId,
|
||||
"userScope": userScope,
|
||||
...headers,
|
||||
},
|
||||
...rest,
|
||||
@@ -88,3 +83,4 @@ export function adjustedData(data, url) {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import { describe, it, expect, vi } from 'vitest';
|
||||
import { adjustedData } from './utils.js';
|
||||
|
||||
describe('adjustedData', () => {
|
||||
vi.doMock('@/utils/constants', () => {
|
||||
return {
|
||||
baseURL: "unit-testing",
|
||||
};
|
||||
});
|
||||
it('should append the URL and process directory data correctly', () => {
|
||||
const input = {
|
||||
type: "directory",
|
||||
@@ -89,9 +94,3 @@ describe('adjustedData', () => {
|
||||
|
||||
});
|
||||
|
||||
vi.mock('@/utils/constants', () => {
|
||||
return {
|
||||
baseURL: "unit-testing",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="breadcrumbs">
|
||||
<component
|
||||
:is="element"
|
||||
:to="base || ''"
|
||||
:to="homePath"
|
||||
:aria-label="$t('files.home')"
|
||||
:title="$t('files.home')"
|
||||
>
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<script>
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { extractSourceFromPath, removePrefix, removeLeadingSlash } from "@/utils/url.js";
|
||||
import Action from "@/components/Action.vue";
|
||||
|
||||
export default {
|
||||
@@ -43,28 +43,25 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
gallerySize: state.user.gallerySize,
|
||||
homePath: "/files/",
|
||||
path: "",
|
||||
};
|
||||
},
|
||||
props: ["base", "noLink"],
|
||||
mounted() {
|
||||
this.updatePaths();
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.updatePaths();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isCardView() {
|
||||
return getters.isCardView();
|
||||
},
|
||||
items() {
|
||||
let relativePath = removePrefix(state.route.path, "files");
|
||||
if (getters.currentView() == "share") {
|
||||
// Split the path, filter out any empty elements, then join again with slashes
|
||||
relativePath = removePrefix(state.route.path, "share");
|
||||
}
|
||||
let parts = relativePath.split("/");
|
||||
|
||||
if (parts[0] === "") {
|
||||
parts.shift();
|
||||
}
|
||||
if (getters.currentView() == "share") {
|
||||
parts.shift();
|
||||
}
|
||||
|
||||
let parts = removeLeadingSlash(this.path).split("/");
|
||||
if (parts[parts.length - 1] === "") {
|
||||
parts.pop();
|
||||
}
|
||||
@@ -104,11 +101,25 @@ export default {
|
||||
},
|
||||
showShare() {
|
||||
return (
|
||||
state.user?.perm && state.user?.perm.share && state.user.username != "publicUser" && getters.currentView() != "share"
|
||||
state.user?.perm &&
|
||||
state.user?.perm.share &&
|
||||
state.user.username != "publicUser" &&
|
||||
getters.currentView() != "share"
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updatePaths() {
|
||||
const result = extractSourceFromPath(getters.routePath());
|
||||
this.path = removePrefix(result.path, "files");
|
||||
if (getters.currentView() == "share") {
|
||||
this.homePath = getters.sharePathBase();
|
||||
this.path = removePrefix(getters.routePath(), this.homePath + "/");
|
||||
}
|
||||
if (state.sources.count > 1) {
|
||||
this.homePath = "/files/" + result.source;
|
||||
}
|
||||
},
|
||||
updateGallerySize(event) {
|
||||
this.gallerySize = parseInt(event.target.value, 10);
|
||||
},
|
||||
|
||||
@@ -15,34 +15,35 @@
|
||||
</div>
|
||||
|
||||
<action
|
||||
v-if="!showCreate"
|
||||
v-if="!showCreate && !isSearchActive && userPerms.modify"
|
||||
icon="add"
|
||||
label="New"
|
||||
@action="startShowCreate"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="showCreate && !isSearchActive"
|
||||
v-if="showCreate && !isSearchActive && userPerms.modify"
|
||||
icon="create_new_folder"
|
||||
:label="$t('sidebar.newFolder')"
|
||||
@action="showHover('newDir')"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="showCreate && !isSearchActive"
|
||||
v-if="showCreate && userPerms.modify && !isSearchActive"
|
||||
icon="note_add"
|
||||
:label="$t('sidebar.newFile')"
|
||||
@action="showHover('newFile')"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="showCreate && !isSearchActive"
|
||||
v-if="showCreate && userPerms.modify && !isSearchActive"
|
||||
icon="file_upload"
|
||||
:label="$t('buttons.upload')"
|
||||
@action="uploadFunc"
|
||||
/>
|
||||
|
||||
<action
|
||||
v-if="!showCreate && headerButtons.select"
|
||||
v-if="!showCreate && selectedCount == 1"
|
||||
icon="info"
|
||||
:label="$t('buttons.info')"
|
||||
show="info"
|
||||
@@ -54,38 +55,38 @@
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && headerButtons.download"
|
||||
v-if="!showCreate && selectedCount > 0"
|
||||
icon="file_download"
|
||||
:label="$t('buttons.download')"
|
||||
@action="startDownload"
|
||||
:counter="selectedCount"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && headerButtons.share"
|
||||
v-if="!showCreate && selectedCount == 1 && userPerms.share"
|
||||
icon="share"
|
||||
:label="$t('buttons.share')"
|
||||
show="share"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && headerButtons.rename && !isSearchActive"
|
||||
v-if="!showCreate && selectedCount == 1 && userPerms.modify && !isSearchActive"
|
||||
icon="mode_edit"
|
||||
:label="$t('buttons.rename')"
|
||||
show="rename"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && headerButtons.copy"
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
icon="content_copy"
|
||||
:label="$t('buttons.copyFile')"
|
||||
show="copy"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && headerButtons.move"
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
icon="forward"
|
||||
:label="$t('buttons.moveFile')"
|
||||
show="move"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && headerButtons.delete"
|
||||
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
|
||||
icon="delete"
|
||||
:label="$t('buttons.delete')"
|
||||
show="delete"
|
||||
@@ -99,7 +100,6 @@ import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import Action from "@/components/Action.vue";
|
||||
import { onlyOfficeUrl } from "@/utils/constants.js";
|
||||
|
||||
|
||||
export default {
|
||||
name: "ContextMenu",
|
||||
components: {
|
||||
@@ -151,21 +151,16 @@ export default {
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
},
|
||||
headerButtons() {
|
||||
return {
|
||||
select: state.selected.length > 0,
|
||||
upload: state.user.perm?.create && state.selected.length > 0,
|
||||
download: state.user.perm.download && state.selected.length > 0,
|
||||
delete: state.selected.length > 0 && state.user.perm.delete,
|
||||
rename: state.selected.length === 1 && state.user.perm.rename,
|
||||
share: state.selected.length === 1 && state.user.perm.share,
|
||||
move: state.selected.length > 0 && state.user.perm.rename,
|
||||
copy: state.selected.length > 0 && state.user.perm?.create,
|
||||
};
|
||||
},
|
||||
selectedCount() {
|
||||
return getters.selectedCount();
|
||||
},
|
||||
userPerms() {
|
||||
return {
|
||||
upload: state.user.perm?.modify && state.selected.length > 0,
|
||||
share: state.user.perm.share,
|
||||
modify: state.user.perm.modify,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
startShowCreate() {
|
||||
|
||||
@@ -2,41 +2,19 @@
|
||||
since it didnt support Vue 3 but the component itself does
|
||||
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<div class="vue-simple-progress" :style="progress_style">
|
||||
<div
|
||||
class="vue-simple-progress-text"
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'top'"
|
||||
v-if="text.length > 0 && textPosition == 'middle'"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
<div class="vue-simple-progress" :style="progress_style">
|
||||
<div
|
||||
class="vue-simple-progress-text"
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'middle'"
|
||||
>
|
||||
|
||||
<div class="vue-simple-progress-bar" :style="bar_style">
|
||||
<div :style="text_style" v-if="text.length > 0 && textPosition == 'inside'">
|
||||
{{ text }}
|
||||
</div>
|
||||
<div
|
||||
style="position: relative; left: -9999px"
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'inside'"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
<div class="vue-simple-progress-bar" :style="bar_style">
|
||||
<div :style="text_style" v-if="text.length > 0 && textPosition == 'inside'">
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="vue-simple-progress-text"
|
||||
:style="text_style"
|
||||
v-if="text.length > 0 && textPosition == 'bottom'"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -117,7 +95,7 @@ export default {
|
||||
case "medium":
|
||||
return 8;
|
||||
case "large":
|
||||
return 12;
|
||||
return 10;
|
||||
case "big":
|
||||
return 16;
|
||||
case "huge":
|
||||
@@ -162,7 +140,6 @@ export default {
|
||||
};
|
||||
|
||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
||||
style["position"] = "relative";
|
||||
style["min-height"] = this.size_px + "px";
|
||||
style["z-index"] = "-2";
|
||||
}
|
||||
@@ -185,9 +162,10 @@ export default {
|
||||
}
|
||||
|
||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
||||
style["position"] = "absolute";
|
||||
//style["position"] = "absolute";
|
||||
style["top"] = "0";
|
||||
style["height"] = "100%";
|
||||
style["min-width"] = "1.5em";
|
||||
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
|
||||
}
|
||||
|
||||
@@ -212,7 +190,6 @@ export default {
|
||||
this.textPosition == "inside"
|
||||
)
|
||||
style["padding-top"] = this.text_padding + "px";
|
||||
|
||||
return style;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,7 +35,24 @@
|
||||
|
||||
<!-- Search results for desktop -->
|
||||
<div v-show="active" id="results" ref="result">
|
||||
<div aria-label="search-path" class="searchContext">Search Context: {{ getContext }}</div>
|
||||
<div class="inputWrapper" style="display: flex">
|
||||
<select
|
||||
v-if="multipleSources"
|
||||
class="searchContext input"
|
||||
aria-label="search-path"
|
||||
v-model="selectedSource"
|
||||
:value="selectedSource"
|
||||
@change="updateSource"
|
||||
>
|
||||
<option v-for="(info, name) in sourceInfo" :key="info" :value="name">
|
||||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Formatted display of selected value -->
|
||||
<div class="searchContext">{{ contextText }}</div>
|
||||
</div>
|
||||
|
||||
<div id="result-list">
|
||||
<div>
|
||||
<div v-if="active">
|
||||
@@ -146,8 +163,7 @@ import ButtonGroup from "./ButtonGroup.vue";
|
||||
import { search } from "@/api";
|
||||
import { getters, mutations, state } from "@/store";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { removeTrailingSlash, removeLeadingSlash } from "@/utils/url";
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { url } from "@/utils/";
|
||||
|
||||
import Icon from "@/components/Icon.vue";
|
||||
|
||||
@@ -193,6 +209,7 @@ export default {
|
||||
reload: false,
|
||||
scrollable: null,
|
||||
hiddenOptions: true,
|
||||
selectedSource: "",
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -215,6 +232,9 @@ export default {
|
||||
mutations.closeHovers();
|
||||
return;
|
||||
}
|
||||
if (state.sources.count > 1) {
|
||||
this.selectedSource = state.sources.current;
|
||||
}
|
||||
setTimeout(() => {
|
||||
resultList.classList.add("active");
|
||||
document.getElementById("main-input").focus();
|
||||
@@ -227,6 +247,11 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (state.sources.count > 1) {
|
||||
this.selectedSource = state.sources.current;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showOptions() {
|
||||
return !this.hiddenOptions || !this.isMobile;
|
||||
@@ -266,9 +291,6 @@ export default {
|
||||
searchHelp() {
|
||||
return this.showHelp;
|
||||
},
|
||||
getContext() {
|
||||
return this.getRelativeContext();
|
||||
},
|
||||
activeStates() {
|
||||
// Create a Set of combined `name` and `type` keys for efficient lookup
|
||||
const selectedSet = new Set(
|
||||
@@ -278,13 +300,31 @@ export default {
|
||||
// Build a map of active states for the `results` array
|
||||
return result;
|
||||
},
|
||||
sourceInfo() {
|
||||
return state.sources.info;
|
||||
},
|
||||
multipleSources() {
|
||||
return Object.keys(state.sources.info).length > 1;
|
||||
},
|
||||
getContext() {
|
||||
let result = url.extractSourceFromPath(decodeURIComponent(state.route.path));
|
||||
if (this.selectedSource === "" || result.source === this.selectedSource) {
|
||||
return result.path;
|
||||
} else {
|
||||
return "/"; // if searching on non-current source, search the whole thing
|
||||
}
|
||||
},
|
||||
contextText() {
|
||||
return `Search Context: ${this.getContext}`; // FIX: Use `this.getContext` as a property, not a function call
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getRelativeContext() {
|
||||
return removePrefix(decodeURIComponent(state.route.path), "files");
|
||||
updateSource(event) {
|
||||
this.selectedSource = event.target.value; // Correct way to get the new value
|
||||
this.submit();
|
||||
},
|
||||
getRelative(path) {
|
||||
return removeTrailingSlash(window.location.href) + "/" + removeLeadingSlash(path);
|
||||
return "/files/" + this.selectedSource + "/" + path;
|
||||
},
|
||||
getIcon(mimetype) {
|
||||
return getMaterialIconForType(mimetype);
|
||||
@@ -388,10 +428,12 @@ export default {
|
||||
if (this.smallerThan != "") {
|
||||
searchTypesFull = searchTypesFull + "type:smallerThan=" + this.smallerThan + " ";
|
||||
}
|
||||
let path = state.route.path;
|
||||
this.ongoing = true;
|
||||
|
||||
this.results = await search(path, searchTypesFull + this.value);
|
||||
let source = this.selectedSource;
|
||||
if (source == "") {
|
||||
source = state.sources.current;
|
||||
}
|
||||
this.results = await search(this.getContext, source, searchTypesFull + this.value);
|
||||
|
||||
this.ongoing = false;
|
||||
if (this.results.length == 0) {
|
||||
@@ -405,8 +447,8 @@ export default {
|
||||
mutations.closeHovers();
|
||||
},
|
||||
addSelected(event, s) {
|
||||
const pathParts = s.path.split("/");
|
||||
const path = removePrefix(decodeURIComponent(state.route.path), "files") + s.path;
|
||||
const pathParts = url.removeTrailingSlash(s.path).split("/");
|
||||
const path = this.getContext + s.path;
|
||||
const modifiedItem = {
|
||||
name: pathParts.pop(),
|
||||
path: path,
|
||||
@@ -438,6 +480,15 @@ export default {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.searchContext.input {
|
||||
background-color: var(--primaryColor) !important;
|
||||
border-radius: 0em;
|
||||
border: unset;
|
||||
width: 25%;
|
||||
min-width: 7em;
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
#results > #result-list {
|
||||
max-height: 80vh;
|
||||
width: 35em;
|
||||
@@ -735,7 +786,7 @@ body.rtl #search .boxes h3 {
|
||||
}
|
||||
|
||||
.filesize {
|
||||
background-color: var(--surfaceSecondary);
|
||||
background: var(--alt-background);
|
||||
border-radius: 1em;
|
||||
padding: 0.25em;
|
||||
padding-left: 0.5em;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
item: true,
|
||||
'no-select': true,
|
||||
activebutton: isMaximized && isSelected,
|
||||
hiddenFile: isHiddenNotSelected
|
||||
hiddenFile: isHiddenNotSelected,
|
||||
}"
|
||||
:id="getID"
|
||||
role="button"
|
||||
@@ -26,7 +26,6 @@
|
||||
@mouseup="cancelContext($event)"
|
||||
>
|
||||
<div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }">
|
||||
|
||||
<img
|
||||
v-if="
|
||||
readOnly === undefined &&
|
||||
@@ -42,14 +41,24 @@
|
||||
</div>
|
||||
|
||||
<div class="text" :class="{ activecontent: isMaximized && isSelected }">
|
||||
<p :class="{'adjustment':quickDownloadEnabled}" class="name">{{ name }}</p>
|
||||
<p class="size" :class="{'adjustment':quickDownloadEnabled}" :data-order="humanSize()">{{ humanSize() }}</p>
|
||||
<p :class="{ adjustment: quickDownloadEnabled }" class="name">{{ name }}</p>
|
||||
<p
|
||||
class="size"
|
||||
:class="{ adjustment: quickDownloadEnabled }"
|
||||
:data-order="humanSize()"
|
||||
>
|
||||
{{ humanSize() }}
|
||||
</p>
|
||||
<p class="modified">
|
||||
<time :datetime="modified">{{ getTime() }}</time>
|
||||
</p>
|
||||
</div>
|
||||
<Icon @click="downloadFile" v-if="quickDownloadEnabled" mimetype="file_download" style="padding-right: 0.5em" />
|
||||
|
||||
<Icon
|
||||
@click="downloadFile"
|
||||
v-if="quickDownloadEnabled"
|
||||
mimetype="file_download"
|
||||
style="padding-right: 0.5em"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -118,14 +127,14 @@ export default {
|
||||
"index",
|
||||
"readOnly",
|
||||
"path",
|
||||
"hidden"
|
||||
"hidden",
|
||||
],
|
||||
computed: {
|
||||
quickDownloadEnabled() {
|
||||
return state.user?.quickDownload;
|
||||
},
|
||||
isHiddenNotSelected() {
|
||||
return !this.isSelected && this.hidden
|
||||
return !this.isSelected && this.hidden;
|
||||
},
|
||||
getID() {
|
||||
return url.base64Encode(encodeURIComponent(this.name));
|
||||
@@ -161,11 +170,8 @@ export default {
|
||||
return true;
|
||||
},
|
||||
thumbnailUrl() {
|
||||
let path = state.req.path;
|
||||
if (state.req.path == "/") {
|
||||
path = "";
|
||||
}
|
||||
return filesApi.getPreviewURL(path + "/" + this.name, "small", state.req.modified);
|
||||
let path = url.removeTrailingSlash(state.req.path) + "/" + this.name;
|
||||
return filesApi.getPreviewURL(state.req.source, path, "small", state.req.modified);
|
||||
},
|
||||
isThumbsEnabled() {
|
||||
return enableThumbs;
|
||||
@@ -193,7 +199,7 @@ export default {
|
||||
event.preventDefault();
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(this.index);
|
||||
downloadFiles()
|
||||
downloadFiles();
|
||||
},
|
||||
handleTouchMove(event) {
|
||||
if (!state.isSafari) return;
|
||||
@@ -446,5 +452,4 @@ export default {
|
||||
.hiddenFile {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
class="card-action"
|
||||
style="display: flex; align-items: center; justify-content: space-between"
|
||||
>
|
||||
<template v-if="user.perm.create">
|
||||
<template v-if="user.perm.modify">
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="$refs.fileList.createDir()"
|
||||
@@ -76,14 +76,14 @@ export default {
|
||||
if (state.isSearchActive) {
|
||||
this.items = [
|
||||
{
|
||||
from: "/files" + state.selected[0].path,
|
||||
from: state.selected[0].url,
|
||||
name: state.selected[0].name,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].path,
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
permissions: permissionsString,
|
||||
};
|
||||
|
||||
usersApi.createApiKey(params);
|
||||
await usersApi.createApiKey(params);
|
||||
notify.showSuccess("successfully created!");
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
|
||||
@@ -70,14 +70,14 @@ export default {
|
||||
|
||||
try {
|
||||
if (state.isSearchActive) {
|
||||
await filesApi.remove(state.selected[0].path);
|
||||
await filesApi.remove(state.selected[0].url);
|
||||
buttons.success("delete");
|
||||
notify.showSuccess("Deleted item successfully");
|
||||
mutations.closeHovers();
|
||||
return;
|
||||
}
|
||||
if (!this.isListing) {
|
||||
await filesApi.remove(state.route.path);
|
||||
await filesApi.remove(state.req.items[state.selected[0]].url);
|
||||
buttons.success("delete");
|
||||
notify.showSuccess("Deleted item successfully");
|
||||
|
||||
@@ -94,14 +94,13 @@ export default {
|
||||
|
||||
let promises = [];
|
||||
for (let index of state.selected) {
|
||||
promises.push(filesApi.remove(state.req.items[index].path));
|
||||
promises.push(filesApi.remove(state.req.items[index].url));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
buttons.success("delete");
|
||||
notify.showSuccess("Deleted item successfully! reloading...");
|
||||
mutations.setReload(true); // Handle reload as neededs
|
||||
|
||||
} catch (e) {
|
||||
buttons.done("delete");
|
||||
notify.showError(e);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
class="card-action"
|
||||
style="display: flex; align-items: center; justify-content: space-between"
|
||||
>
|
||||
<template v-if="user.perm.create">
|
||||
<template v-if="user.perm.modify">
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="$refs.fileList.createDir()"
|
||||
@@ -75,14 +75,14 @@ export default {
|
||||
if (state.isSearchActive) {
|
||||
this.items = [
|
||||
{
|
||||
from: "/files" + state.selected[0].path,
|
||||
from: state.selected[0].url,
|
||||
name: state.selected[0].name,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].path,
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
|
||||
@@ -73,8 +73,9 @@ export default {
|
||||
if (event.keyCode === 27) {
|
||||
event.stopImmediatePropagation();
|
||||
mutations.closeHovers();
|
||||
} else if (event.KeyCode === 8) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Enter
|
||||
if (event.keyCode === 13) {
|
||||
switch (currentPrompt.name) {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
<h2>{{ $t("buttons.share") }}</h2>
|
||||
</div>
|
||||
<div aria-label="share-paths" class="searchContext">Path: {{ subpath }}</div>
|
||||
<p>Note: anyone who has access to the link (and optional password) can access the shared files. There is no need to be logged in.</p>
|
||||
<p>
|
||||
Note: anyone who has access to the link (and optional password) can access the
|
||||
shared files. There is no requirement to be logged in.
|
||||
</p>
|
||||
<template v-if="listing">
|
||||
<div class="card-content">
|
||||
<table>
|
||||
@@ -126,6 +129,7 @@ import { notify } from "@/notify";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { shareApi, publicApi } from "@/api";
|
||||
import { fromNow } from "@/utils/moment";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
import Clipboard from "clipboard";
|
||||
|
||||
export default {
|
||||
@@ -137,6 +141,7 @@ export default {
|
||||
links: [],
|
||||
clip: null,
|
||||
subpath: "",
|
||||
source: "",
|
||||
password: "",
|
||||
listing: true,
|
||||
};
|
||||
@@ -173,15 +178,27 @@ export default {
|
||||
},
|
||||
async beforeMount() {
|
||||
try {
|
||||
let path = getters.routePath("files");
|
||||
if (state.isSearchActive) {
|
||||
this.subpath = state.selected[0].path;
|
||||
path = state.selected[0].path;
|
||||
this.source = state.selected[0].source;
|
||||
} else {
|
||||
let path = "." + getters.routePath("files");
|
||||
this.source = state.req.source;
|
||||
if (getters.selectedCount() === 1) {
|
||||
path = path + state.req.items[this.selected[0]].name;
|
||||
if (state.sources.count > 1) {
|
||||
path = removePrefix(
|
||||
state.req.items[this.selected[0]].url,
|
||||
"files/" + state.req.source
|
||||
);
|
||||
} else {
|
||||
path = removePrefix(state.req.items[this.selected[0]].url, "files");
|
||||
}
|
||||
}
|
||||
if (state.sources.count > 1) {
|
||||
path = removePrefix(path, state.req.source);
|
||||
}
|
||||
this.subpath = decodeURIComponent(path);
|
||||
}
|
||||
this.subpath = decodeURIComponent(path);
|
||||
// get last element of the path
|
||||
const links = await shareApi.get(this.subpath);
|
||||
this.links = links;
|
||||
@@ -211,10 +228,11 @@ export default {
|
||||
let res = null;
|
||||
|
||||
if (isPermanent) {
|
||||
res = await shareApi.create(this.subpath, this.password);
|
||||
res = await shareApi.create(this.subpath, this.source, this.password);
|
||||
} else {
|
||||
res = await shareApi.create(
|
||||
this.subpath,
|
||||
this.source,
|
||||
this.password,
|
||||
this.time.toString(),
|
||||
this.unit
|
||||
@@ -251,6 +269,8 @@ export default {
|
||||
return this.selected.length === 1 && !state.req.items[this.selected[0]].isDir;
|
||||
},
|
||||
buildDownloadLink(share) {
|
||||
share.source = this.source;
|
||||
share.path = this.subpath;
|
||||
return publicApi.getDownloadURL(share);
|
||||
},
|
||||
sort() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p>{{ $t("prompts.deleteMessageShare", { path: "" }) }}</p>
|
||||
<p>{{ $t("prompts.deleteMessageShare", { path }) }}</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button
|
||||
@@ -24,10 +24,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getters } from "@/store"; // Import your custom store
|
||||
import { getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
export default {
|
||||
name: "share-delete",
|
||||
props: {
|
||||
path: String,
|
||||
},
|
||||
computed: {
|
||||
closeHovers() {
|
||||
return mutations.closeHovers();
|
||||
|
||||
@@ -2,36 +2,14 @@
|
||||
<div>
|
||||
<h3>{{ $t("settings.permissions") }}</h3>
|
||||
<p class="small">{{ $t("settings.permissionsHelp") }}</p>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" v-model="admin" />
|
||||
{{ $t("settings.administrator") }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.create" />
|
||||
{{ $t("settings.perm.create") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.delete" />
|
||||
{{ $t("settings.perm.delete") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.download" />
|
||||
{{ $t("settings.perm.download") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.modify" />
|
||||
{{ $t("settings.perm.modify") }}
|
||||
</p>
|
||||
<p v-if="isExecEnabled">
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.execute" />
|
||||
{{ $t("settings.perm.execute") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.rename" />
|
||||
{{ $t("settings.perm.rename") }}
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" :disabled="admin" v-model="perm.share" />
|
||||
{{ $t("settings.perm.share") }}
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
placeholder="enter new password"
|
||||
v-model="user.password"
|
||||
id="password"
|
||||
@input="emitUpdate"
|
||||
@input="setUpdatePassword"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p v-if="!isDefault">
|
||||
<p>
|
||||
<label for="username">{{ $t("settings.username") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
@@ -22,7 +22,7 @@
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p v-if="!isDefault">
|
||||
<p>
|
||||
<label for="password">{{ $t("settings.password") }}</label>
|
||||
<input
|
||||
class="input input--block"
|
||||
@@ -34,18 +34,57 @@
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="scope">{{ $t("settings.scope") }}</label>
|
||||
<p v-if="!isNew">
|
||||
<input
|
||||
:disabled="createUserDir"
|
||||
:placeholder="scopePlaceholder"
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model="user.scope"
|
||||
id="scope"
|
||||
type="checkbox"
|
||||
:checked="updatePassword"
|
||||
@change="(event) => $emit('update:updatePassword', event.target.checked)"
|
||||
/>
|
||||
Change password on save
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input
|
||||
type="checkbox"
|
||||
:disabled="!stateUser.perm?.admin"
|
||||
v-model="user.lockPassword"
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
{{ $t("settings.lockPassword") }}
|
||||
</p>
|
||||
|
||||
<label for="scopes">{{ $t("settings.scopes") }}</label>
|
||||
<div class="scope-list" v-for="(source, index) in selectedSources" :key="index">
|
||||
<!-- Select dropdown -->
|
||||
<select class="input flat-right" v-model="source.name">
|
||||
<option v-for="s in sourceList" :key="s" :value="s.name">
|
||||
{{ s.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Input field for scope, bound to the selectedSources array -->
|
||||
<input
|
||||
class="input flat-left scope-input"
|
||||
placeholder="scope eg. 'subfolder', leave blank for root"
|
||||
@input="updateParent({ source: source, input: $event })"
|
||||
:value="source.scope"
|
||||
:class="{ 'flat-right': index != 0 }"
|
||||
/>
|
||||
<!-- Remove button -->
|
||||
<button
|
||||
v-if="index != 0"
|
||||
class="button flat-left no-height"
|
||||
@click="removeScope(index)"
|
||||
>
|
||||
<i class="material-icons material-size">delete</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Button to add more sources -->
|
||||
<button v-if="hasMoreSources" @click="addNewScopeSource" class="button no-height">
|
||||
<i class="material-icons material-size">add</i>
|
||||
</button>
|
||||
|
||||
<p class="small" v-if="displayHomeDirectoryCheckbox">
|
||||
<input type="checkbox" v-model="createUserDir" />
|
||||
{{ $t("settings.createUserHomeDirectory") }}
|
||||
@@ -61,16 +100,6 @@
|
||||
></languages>
|
||||
</p>
|
||||
|
||||
<p v-if="!isDefault">
|
||||
<input
|
||||
type="checkbox"
|
||||
:disabled="stateUser.perm?.admin"
|
||||
v-model="user.lockPassword"
|
||||
@input="emitUpdate"
|
||||
/>
|
||||
{{ $t("settings.lockPassword") }}
|
||||
</p>
|
||||
|
||||
<permissions :perm="localUser.perm" />
|
||||
<commands v-if="isExecEnabled" v-model:commands="user.commands" />
|
||||
</div>
|
||||
@@ -82,6 +111,7 @@ import Permissions from "./Permissions.vue";
|
||||
import Commands from "./Commands.vue";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
import { state } from "@/store";
|
||||
import { settingsApi } from "@/api";
|
||||
|
||||
export default {
|
||||
name: "UserForm",
|
||||
@@ -95,31 +125,60 @@ export default {
|
||||
createUserDir: false,
|
||||
originalUserScope: ".",
|
||||
localUser: { ...this.user },
|
||||
sourceList: [],
|
||||
availableSources: [],
|
||||
selectedSources: [],
|
||||
};
|
||||
},
|
||||
props: {
|
||||
user: Object, // Define user as a prop
|
||||
isDefault: Boolean,
|
||||
updatePassword: Boolean,
|
||||
isNew: Boolean,
|
||||
},
|
||||
async mounted() {
|
||||
if (!this.stateUser.perm.admin) {
|
||||
this.sourceList = this.user.scopes;
|
||||
} else {
|
||||
this.sourceList = await settingsApi.get("sources");
|
||||
}
|
||||
this.localUser = { ...this.user };
|
||||
this.selectedSources = [];
|
||||
this.availableSources = [...this.sourceList];
|
||||
if (this.isNew) {
|
||||
const newSource = this.availableSources.shift(); // Take the first item instead of last
|
||||
if (newSource) {
|
||||
this.selectedSources.push(newSource);
|
||||
}
|
||||
} else {
|
||||
// Populate selectedSources with existing user scopes
|
||||
if (this.user.scopes) {
|
||||
this.selectedSources = this.user.scopes;
|
||||
// remove items with same name from availableSources
|
||||
for (const source of this.selectedSources) {
|
||||
this.availableSources = this.availableSources.filter(
|
||||
(s) => source.name != s.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
user: {
|
||||
handler(newUser) {
|
||||
this.localUser = { ...newUser }; // Watch for changes in the parent and update the local copy
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
"user.perm.admin": function (newValue) {
|
||||
if (newValue) {
|
||||
this.user.lockPassword = false;
|
||||
}
|
||||
},
|
||||
createUserDir(newVal) {
|
||||
this.user.scope = newVal ? "" : this.originalUserScope;
|
||||
this.user.scopes = newVal ? { default: "" } : this.originalUserScope;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasMoreSources() {
|
||||
return this.availableSources.length > 0;
|
||||
},
|
||||
sourceInfo() {
|
||||
return state.sources.info;
|
||||
},
|
||||
stateUser() {
|
||||
return state.user;
|
||||
},
|
||||
@@ -138,5 +197,70 @@ export default {
|
||||
return enableExec; // Removed arrow function
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setUpdatePassword() {
|
||||
this.$emit("update:updatePassword", true);
|
||||
},
|
||||
updateParent(input) {
|
||||
let updatedScopes = {};
|
||||
// Update the selectedSources array directly
|
||||
this.selectedSources.forEach((source) => {
|
||||
if (source.name === input.source.name) {
|
||||
updatedScopes[source.name] = input.input.target.value;
|
||||
} else {
|
||||
updatedScopes[source.name] = source.scope;
|
||||
}
|
||||
});
|
||||
let intermediate = [];
|
||||
Object.entries(updatedScopes).forEach(([key, value]) => {
|
||||
intermediate.push({ name: key, scope: value });
|
||||
});
|
||||
let final = [];
|
||||
for (const source of intermediate) {
|
||||
final.push(source);
|
||||
}
|
||||
this.selectedSources = final;
|
||||
this.$emit("update:user", { ...this.user, scopes: this.selectedSources });
|
||||
},
|
||||
addNewScopeSource(event) {
|
||||
event.preventDefault();
|
||||
if (this.availableSources.length > 0) {
|
||||
const newSource = this.availableSources.pop();
|
||||
if (newSource) {
|
||||
this.selectedSources.push({ name: newSource.name, scope: "" });
|
||||
}
|
||||
}
|
||||
},
|
||||
removeScope(index) {
|
||||
const removedSource = this.selectedSources.splice(index, 1)[0];
|
||||
this.availableSources.push({ name: removedSource.name }); // Make source available again
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.scope-list {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flat-right {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.flat-left {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
.scope-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-height {
|
||||
height: unset;
|
||||
}
|
||||
.material-size {
|
||||
font-size: 1em !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<div class="card clickable" style="min-height: 4em">
|
||||
<div class="card" style="min-height: 4em">
|
||||
<div class="card-wrapper user-card">
|
||||
<div @click="navigateTo('/settings#profile-main')" class="inner-card">
|
||||
<i class="material-icons">person</i>
|
||||
{{ user.username }}
|
||||
<i aria-label="settings" class="material-icons">settings</i>
|
||||
<button class="person-button action">
|
||||
<i class="material-icons">person</i>
|
||||
{{ user.username }}
|
||||
<i aria-label="settings" class="material-icons">settings</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inner-card logout-button" @click="logout">
|
||||
<i v-if="canLogout" class="material-icons">exit_to_app</i>
|
||||
<div class="inner-card" @click="logout">
|
||||
<button aria-label="logout-button" class="logout-button action">
|
||||
<i v-if="canLogout" class="material-icons">exit_to_app</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,24 +47,32 @@
|
||||
</div>
|
||||
|
||||
<!-- Section for logged-in users -->
|
||||
<div v-if="loginCheck" class="sidebar-scroll-list">
|
||||
<div v-if="loginCheck && !disableUsedPercentage" class="sidebar-scroll-list">
|
||||
<div class="sources card">
|
||||
<span>Sources</span>
|
||||
<div class="inner-card">
|
||||
<!-- My Files button -->
|
||||
<button
|
||||
class="action"
|
||||
@click="navigateTo('/files/')"
|
||||
v-for="(info, name) in sourceInfo"
|
||||
:key="name"
|
||||
class="action source-button"
|
||||
:class="{ active: activeSource == name }"
|
||||
@click="navigateTo('/files/' + info.pathPrefix)"
|
||||
:aria-label="$t('sidebar.myFiles')"
|
||||
title="default"
|
||||
:title="name"
|
||||
>
|
||||
<i class="material-icons">folder</i>
|
||||
<span>default</span>
|
||||
<i class="material-icons source-icon">folder</i>
|
||||
<span>{{ name }}</span>
|
||||
<div>
|
||||
<progress-bar :val="usage.usedPercentage" size="medium"></progress-bar>
|
||||
<progress-bar
|
||||
:val="info.usedPercentage"
|
||||
text-position="inside"
|
||||
:text="info.usedPercentage + '%'"
|
||||
size="large"
|
||||
text-fg-color="white"
|
||||
></progress-bar>
|
||||
<div class="usage-info">
|
||||
<span>{{ usage.usedPercentage }}%</span>
|
||||
<span>{{ usage.used }} of {{ usage.total }} used</span>
|
||||
<span>{{ info.used }} of {{ info.total }} used</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -93,9 +105,6 @@ export default {
|
||||
hoverText: "Quick Toggles", // Initially empty
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateUsage();
|
||||
},
|
||||
computed: {
|
||||
isSettings: () => getters.isSettings(),
|
||||
isStickySidebar: () => getters.isStickySidebar(),
|
||||
@@ -112,8 +121,9 @@ export default {
|
||||
disableExternal: () => disableExternal,
|
||||
disableUsedPercentage: () => disableUsedPercentage,
|
||||
canLogout: () => !noAuth && loginPage,
|
||||
usage: () => state.usage,
|
||||
route: () => state.route,
|
||||
sourceInfo: () => state.sources.info,
|
||||
activeSource: () => state.sources.current,
|
||||
},
|
||||
watch: {
|
||||
route() {
|
||||
@@ -125,7 +135,27 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.loginCheck) {
|
||||
this.updateUsage();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateUsage() {
|
||||
if (!disableUsedPercentage) {
|
||||
if (state.sources.info === undefined) {
|
||||
return;
|
||||
}
|
||||
for (const source of Object.keys(state.sources.info)) {
|
||||
let usage = await filesApi.usage(source);
|
||||
let sourceInfo = state.sources.info[source];
|
||||
sourceInfo.used = getHumanReadableFilesize(usage.used);
|
||||
sourceInfo.total = getHumanReadableFilesize(usage.total);
|
||||
sourceInfo.usedPercentage = Math.round((usage.used / usage.total) * 100);
|
||||
mutations.updateSource(source, sourceInfo);
|
||||
}
|
||||
}
|
||||
},
|
||||
checkLogin() {
|
||||
return getters.isLoggedIn() && !getters.routePath().startsWith("/share");
|
||||
},
|
||||
@@ -144,24 +174,6 @@ export default {
|
||||
toggleSticky() {
|
||||
mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar });
|
||||
},
|
||||
async updateUsage() {
|
||||
if (!this.checkLogin()) {
|
||||
return;
|
||||
}
|
||||
let usageStats = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||
if (this.disableUsedPercentage) {
|
||||
return usageStats;
|
||||
}
|
||||
let usage = await filesApi.usage("default");
|
||||
usageStats = {
|
||||
used: getHumanReadableFilesize(usage.used),
|
||||
total: getHumanReadableFilesize(usage.total),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
|
||||
mutations.setUsage(usageStats);
|
||||
},
|
||||
|
||||
navigateTo(path) {
|
||||
const hashIndex = path.indexOf("#");
|
||||
if (hashIndex !== -1) {
|
||||
@@ -213,7 +225,6 @@ export default {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
margin-top: 0.5em !important;
|
||||
}
|
||||
|
||||
.sources .inner-card {
|
||||
@@ -250,9 +261,26 @@ button.action {
|
||||
background-color: var(--primaryColor) !important;
|
||||
border-radius: 10em;
|
||||
}
|
||||
|
||||
.inner-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.source-button {
|
||||
margin-top: 0.5em !important;
|
||||
}
|
||||
|
||||
.source-button.active {
|
||||
background: var(--alt-background);
|
||||
}
|
||||
.source-icon {
|
||||
padding: 0.1em !important;
|
||||
}
|
||||
|
||||
.logout-button,
|
||||
.person-button {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -109,6 +109,7 @@ body.rtl nav {
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0.5em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"currentlyNavigating": "Currently navigating on:",
|
||||
"deleteMessageMultiple": "Are you sure you want to delete {count} file(s)?",
|
||||
"deleteMessageSingle": "Are you sure you want to delete this file/folder?",
|
||||
"deleteMessageShare": "Are you sure you want to delete this share({path})?",
|
||||
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
|
||||
"deleteTitle": "Delete files",
|
||||
"displayName": "Display Name:",
|
||||
"download": "Download files",
|
||||
@@ -173,6 +173,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"settings": {
|
||||
"scopes": "Define allowed sources and the user scope within them",
|
||||
"UserManagement": "User Management",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrator",
|
||||
@@ -181,7 +182,7 @@
|
||||
"allowNew": "Create new files and directories",
|
||||
"allowPublish": "Publish new posts and pages",
|
||||
"allowSignup": "Allow users to signup",
|
||||
"avoidChanges": "(leave blank to avoid changes)",
|
||||
"avoidChanges": "also tick 'change password' to apply this password",
|
||||
"branding": "Branding",
|
||||
"brandingDirectoryPath": "Branding directory path",
|
||||
"brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"video": "Vídeo"
|
||||
},
|
||||
"settings": {
|
||||
"scopes": "Definir fuentes permitidas y el alcance del usuario dentro de ellas",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrador",
|
||||
"allowCommands": "Ejecutar comandos",
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"video": "Video"
|
||||
},
|
||||
"settings": {
|
||||
"scopes": "Définir les sources autorisées et la portée de l'utilisateur au sein de celles-ci",
|
||||
"admin": "Admin",
|
||||
"administrator": "Administrateur",
|
||||
"allowCommands": "Exécuter des commandes",
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"video": "视频"
|
||||
},
|
||||
"settings": {
|
||||
"scopes": "定义允许的来源及其中的用户范围",
|
||||
"admin": "管理员",
|
||||
"administrator": "管理员",
|
||||
"allowCommands": "执行命令(Shell 命令)",
|
||||
|
||||
@@ -9,7 +9,7 @@ export function showPopup(type, message, autoclose = true) {
|
||||
}
|
||||
|
||||
const [popup, popupContent] = getElements()
|
||||
if (popup === undefined) {
|
||||
if (popup === undefined || popup == null) {
|
||||
return
|
||||
}
|
||||
popup.classList.remove('success', 'error') // Clear previous types
|
||||
|
||||
@@ -2,7 +2,7 @@ import { removePrefix } from "@/utils/url.js";
|
||||
import { getFileExtension } from "@/utils/files.js";
|
||||
import { state } from "./state.js";
|
||||
import { mutations } from "./mutations.js";
|
||||
import { noAuth } from "@/utils/constants.js";
|
||||
import { noAuth,baseURL } from "@/utils/constants.js";
|
||||
|
||||
export const getters = {
|
||||
isCardView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
|
||||
@@ -137,6 +137,13 @@ export const getters = {
|
||||
routePath: (trimModifier="") => {
|
||||
return removePrefix(state.route.path,trimModifier)
|
||||
},
|
||||
sharePathBase: () => {
|
||||
let urlPath = getters.routePath("share");
|
||||
// Step 1: Split the path by '/'
|
||||
let parts = urlPath.split("/");
|
||||
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
|
||||
return baseURL + "share/"+ parts[1];
|
||||
},
|
||||
currentView: () => {
|
||||
const pathname = getters.routePath()
|
||||
if (pathname.startsWith(`/settings`)) {
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import * as i18n from "@/i18n";
|
||||
import { state } from "./state.js";
|
||||
import router from "@/router";
|
||||
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
|
||||
import { usersApi } from "@/api";
|
||||
import { notify } from "@/notify";
|
||||
import { sortedItems } from "@/utils/sort.js";
|
||||
|
||||
export const mutations = {
|
||||
setCurrentSource: (value) => {
|
||||
state.sources.current = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
updateSource: (sourcename,value) => {
|
||||
if (state.sources.info[sourcename]) {
|
||||
state.sources.info[sourcename] = value;
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
setSources: (user) => {
|
||||
const currentSource = user.scopes.length > 0 ? user.scopes[0].name : "";
|
||||
let sources = {info: {}, current: currentSource, count: user.scopes.length};
|
||||
for (const source of user.scopes) {
|
||||
sources.info[source.name] = {
|
||||
pathPrefix: sources.count == 1 ? "" : source.name,
|
||||
used: 0,
|
||||
total: 0,
|
||||
usedPercentage: 0,
|
||||
};
|
||||
}
|
||||
state.sources = sources;
|
||||
emitStateChanged();
|
||||
},
|
||||
setGallerySize: (value) => {
|
||||
state.user.gallerySize = value
|
||||
emitStateChanged();
|
||||
@@ -14,7 +37,8 @@ export const mutations = {
|
||||
},
|
||||
setActiveSettingsView: (value) => {
|
||||
state.activeSettingsView = value;
|
||||
router.push({ hash: "#" + value });
|
||||
// Update the hash in the URL without reloading or changing history state
|
||||
window.history.replaceState(null, "", "#" + value);
|
||||
const element = document.getElementById(value);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
@@ -57,8 +81,8 @@ export const mutations = {
|
||||
state.upload = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
setUsage: (value) => {
|
||||
state.usage = value;
|
||||
setUsage: (source,value) => {
|
||||
state.usages[source] = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
closeHovers: () => {
|
||||
@@ -103,23 +127,26 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
},
|
||||
setCurrentUser: (value) => {
|
||||
localStorage.setItem("userData", undefined);
|
||||
// If value is null or undefined, emit state change and exit early
|
||||
if (!value) {
|
||||
try {
|
||||
localStorage.setItem("userData", undefined);
|
||||
// If value is null or undefined, emit state change and exit early
|
||||
if (!value) {
|
||||
state.user = value;
|
||||
emitStateChanged();
|
||||
return;
|
||||
}
|
||||
mutations.setSources(value);
|
||||
if (value.username != "publicUser") {
|
||||
localStorage.setItem("userData", JSON.stringify(value));
|
||||
}
|
||||
// Ensure locale exists and is valid
|
||||
if (!value.locale) {
|
||||
value.locale = i18n.detectLocale(); // Default to detected locale if missing
|
||||
}
|
||||
state.user = value;
|
||||
emitStateChanged();
|
||||
return;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
if (value.username != "publicUser") {
|
||||
localStorage.setItem("userData", JSON.stringify(value));
|
||||
}
|
||||
// Ensure locale exists and is valid
|
||||
if (!value.locale) {
|
||||
value.locale = i18n.detectLocale(); // Default to detected locale if missing
|
||||
}
|
||||
state.user = value;
|
||||
// Emit state change after setting the user and locale
|
||||
emitStateChanged();
|
||||
},
|
||||
setJWT: (value) => {
|
||||
|
||||
@@ -8,12 +8,13 @@ export const state = reactive({
|
||||
isMobile: window.innerWidth <= 800,
|
||||
isSearchActive: false,
|
||||
showSidebar: false,
|
||||
usage: {
|
||||
used: "0 B",
|
||||
total: "0 B",
|
||||
usedPercentage: 0
|
||||
},
|
||||
usages: {},
|
||||
editor: null,
|
||||
sources: {
|
||||
current: "",
|
||||
count: 1,
|
||||
info: {},
|
||||
},
|
||||
user: {
|
||||
quickDownloadEnabled: false,
|
||||
gallarySize: 0,
|
||||
@@ -22,6 +23,7 @@ export const state = reactive({
|
||||
locale: detectLocale(), // Default to the locale from moment
|
||||
viewMode: 'normal', // Default to mosaic view
|
||||
showHidden: false, // Default to false, assuming this is a boolean
|
||||
scopes: [],
|
||||
perm: {},
|
||||
rules: [], // Default to an empty array
|
||||
permissions: {}, // Default to an empty object for permissions
|
||||
|
||||
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user