├── .build ├── aria2.supervisor.conf ├── build-assets.sh └── entrypoint.sh ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── stale.yml ├── .gitignore ├── .gitmodules ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── README_zh-CN.md ├── SECURITY.md ├── application ├── application.go ├── constants │ ├── constants.go │ └── size.go ├── dependency │ ├── dependency.go │ └── options.go ├── migrator │ ├── avatars.go │ ├── conf │ │ ├── conf.go │ │ └── defaults.go │ ├── directlink.go │ ├── file.go │ ├── folders.go │ ├── group.go │ ├── migrator.go │ ├── model │ │ ├── dialects │ │ │ └── dialect_sqlite.go │ │ ├── file.go │ │ ├── folder.go │ │ ├── group.go │ │ ├── init.go │ │ ├── node.go │ │ ├── policy.go │ │ ├── setting.go │ │ ├── share.go │ │ ├── source_link.go │ │ ├── tag.go │ │ ├── task.go │ │ ├── user.go │ │ └── webdav.go │ ├── node.go │ ├── policy.go │ ├── settings.go │ ├── share.go │ ├── user.go │ └── webdav.go └── statics │ ├── embed.go │ └── statics.go ├── azure-pipelines.yml ├── cmd ├── eject.go ├── migrate.go ├── root.go └── server.go ├── docker-compose.yml ├── ent ├── client.go ├── davaccount.go ├── davaccount │ ├── davaccount.go │ └── where.go ├── davaccount_create.go ├── davaccount_delete.go ├── davaccount_query.go ├── davaccount_update.go ├── directlink.go ├── directlink │ ├── directlink.go │ └── where.go ├── directlink_create.go ├── directlink_delete.go ├── directlink_query.go ├── directlink_update.go ├── ent.go ├── entc.go ├── entity.go ├── entity │ ├── entity.go │ └── where.go ├── entity_create.go ├── entity_delete.go ├── entity_query.go ├── entity_update.go ├── enttest │ └── enttest.go ├── file.go ├── file │ ├── file.go │ └── where.go ├── file_create.go ├── file_delete.go ├── file_query.go ├── file_update.go ├── generate.go ├── group.go ├── group │ ├── group.go │ └── where.go ├── group_create.go ├── group_delete.go ├── group_query.go ├── group_update.go ├── hook │ └── hook.go ├── intercept │ └── intercept.go ├── internal │ └── schema.go ├── metadata.go ├── metadata │ ├── metadata.go │ └── where.go ├── metadata_create.go ├── metadata_delete.go ├── metadata_query.go ├── metadata_update.go ├── migrate │ ├── migrate.go │ └── schema.go ├── mutation.go ├── mutationhelper.go ├── node.go ├── node │ ├── node.go │ └── where.go ├── node_create.go ├── node_delete.go ├── node_query.go ├── node_update.go ├── passkey.go ├── passkey │ ├── passkey.go │ └── where.go ├── passkey_create.go ├── passkey_delete.go ├── passkey_query.go ├── passkey_update.go ├── predicate │ └── predicate.go ├── runtime.go ├── runtime │ └── runtime.go ├── schema │ ├── common.go │ ├── davaccount.go │ ├── directlink.go │ ├── entity.go │ ├── file.go │ ├── group.go │ ├── metadata.go │ ├── node.go │ ├── passkey.go │ ├── policy.go │ ├── setting.go │ ├── share.go │ ├── task.go │ └── user.go ├── setting.go ├── setting │ ├── setting.go │ └── where.go ├── setting_create.go ├── setting_delete.go ├── setting_query.go ├── setting_update.go ├── share.go ├── share │ ├── share.go │ └── where.go ├── share_create.go ├── share_delete.go ├── share_query.go ├── share_update.go ├── storagepolicy.go ├── storagepolicy │ ├── storagepolicy.go │ └── where.go ├── storagepolicy_create.go ├── storagepolicy_delete.go ├── storagepolicy_query.go ├── storagepolicy_update.go ├── task.go ├── task │ ├── task.go │ └── where.go ├── task_create.go ├── task_delete.go ├── task_query.go ├── task_update.go ├── templates │ ├── createhelper.tmpl │ ├── edgehelper.tmpl │ └── mutationhelper.tmpl ├── tx.go ├── user.go ├── user │ ├── user.go │ └── where.go ├── user_create.go ├── user_delete.go ├── user_query.go └── user_update.go ├── giscus.json ├── go.mod ├── go.sum ├── inventory ├── client.go ├── common.go ├── dav_account.go ├── debug │ └── debug.go ├── direct_link.go ├── file.go ├── file_utils.go ├── group.go ├── node.go ├── policy.go ├── setting.go ├── share.go ├── task.go ├── tx.go ├── types │ └── types.go └── user.go ├── main.go ├── middleware ├── auth.go ├── captcha.go ├── cluster.go ├── common.go ├── file.go ├── frontend.go ├── mock.go ├── session.go └── wopi.go ├── pkg ├── auth │ ├── auth.go │ ├── hmac.go │ ├── jwt.go │ └── requestinfo │ │ └── requestinfo.go ├── balancer │ ├── balancer.go │ ├── balancer_test.go │ ├── errors.go │ ├── roundrobin.go │ └── roundrobin_test.go ├── boolset │ ├── boolset.go │ └── boolset_test.go ├── cache │ ├── driver.go │ ├── driver_test.go │ ├── memo.go │ ├── memo_test.go │ ├── redis.go │ └── redis_test.go ├── cluster │ ├── node.go │ ├── pool.go │ └── routes │ │ └── routes.go ├── conf │ ├── conf.go │ ├── conf_test.go │ └── types.go ├── credmanager │ └── credmanager.go ├── crontab │ └── crontab.go ├── downloader │ ├── aria2 │ │ ├── aria2.go │ │ └── rpc │ │ │ ├── README.md │ │ │ ├── call.go │ │ │ ├── client.go │ │ │ ├── const.go │ │ │ ├── json2.go │ │ │ ├── notification.go │ │ │ ├── proc.go │ │ │ ├── proto.go │ │ │ └── resp.go │ ├── downloader.go │ ├── qbittorrent │ │ ├── qbittorrent.go │ │ └── types.go │ └── slave │ │ └── slave.go ├── email │ ├── mail.go │ ├── smtp.go │ └── template.go ├── filemanager │ ├── chunk │ │ ├── backoff │ │ │ └── backoff.go │ │ └── chunk.go │ ├── driver │ │ ├── cos │ │ │ ├── cos.go │ │ │ ├── media.go │ │ │ └── scf.go │ │ ├── handler.go │ │ ├── local │ │ │ ├── entity.go │ │ │ ├── fallocate.go │ │ │ ├── fallocate_darwin.go │ │ │ ├── fallocate_linux.go │ │ │ └── local.go │ │ ├── obs │ │ │ ├── media.go │ │ │ └── obs.go │ │ ├── onedrive │ │ │ ├── api.go │ │ │ ├── client.go │ │ │ ├── oauth.go │ │ │ ├── onedrive.go │ │ │ ├── options.go │ │ │ └── types.go │ │ ├── oss │ │ │ ├── callback.go │ │ │ ├── media.go │ │ │ └── oss.go │ │ ├── qiniu │ │ │ ├── media.go │ │ │ └── qiniu.go │ │ ├── remote │ │ │ ├── client.go │ │ │ └── remote.go │ │ ├── s3 │ │ │ └── s3.go │ │ ├── upyun │ │ │ ├── media.go │ │ │ └── upyun.go │ │ └── util.go │ ├── fs │ │ ├── dbfs │ │ │ ├── dbfs.go │ │ │ ├── file.go │ │ │ ├── global.go │ │ │ ├── lock.go │ │ │ ├── manage.go │ │ │ ├── my_navigator.go │ │ │ ├── navigator.go │ │ │ ├── options.go │ │ │ ├── share_navigator.go │ │ │ ├── sharewithme_navigator.go │ │ │ ├── trash_navigator.go │ │ │ ├── upload.go │ │ │ └── validator.go │ │ ├── fs.go │ │ ├── mime │ │ │ └── mime.go │ │ └── uri.go │ ├── lock │ │ ├── memlock.go │ │ └── memlock_test.go │ ├── manager │ │ ├── archive.go │ │ ├── entity.go │ │ ├── entitysource │ │ │ └── entitysource.go │ │ ├── fs.go │ │ ├── manager.go │ │ ├── mediameta.go │ │ ├── metadata.go │ │ ├── operation.go │ │ ├── recycle.go │ │ ├── thumbnail.go │ │ ├── upload.go │ │ └── viewer.go │ └── workflows │ │ ├── archive.go │ │ ├── extract.go │ │ ├── import.go │ │ ├── remote_download.go │ │ ├── upload.go │ │ └── worfklows.go ├── hashid │ └── hash.go ├── logging │ └── logger.go ├── mediameta │ ├── exif.go │ ├── extractor.go │ ├── ffprobe.go │ └── music.go ├── queue │ ├── metric.go │ ├── options.go │ ├── queue.go │ ├── registry.go │ ├── scheduler.go │ ├── task.go │ └── thread.go ├── recaptcha │ └── recaptcha.go ├── request │ ├── options.go │ ├── request.go │ ├── request_test.go │ ├── tpslimiter.go │ ├── tpslimiter_test.go │ └── utils.go ├── serializer │ ├── auth.go │ ├── error.go │ ├── response.go │ ├── setting.go │ └── upload.go ├── sessionstore │ ├── kv.go │ └── sessionstore.go ├── setting │ ├── adapters.go │ ├── provider.go │ └── types.go ├── thumb │ ├── builtin.go │ ├── ffmpeg.go │ ├── libreoffice.go │ ├── music.go │ ├── pipeline.go │ ├── tester.go │ └── vips.go ├── util │ ├── common.go │ ├── common_test.go │ ├── io.go │ ├── io_test.go │ ├── logger.go │ ├── logger_test.go │ ├── path.go │ ├── path_test.go │ └── session.go ├── webdav │ ├── file.go │ ├── if.go │ ├── internal │ │ └── xml │ │ │ ├── README │ │ │ ├── marshal.go │ │ │ ├── read.go │ │ │ ├── typeinfo.go │ │ │ └── xml.go │ ├── lock.go │ ├── prop.go │ ├── webdav.go │ └── xml.go └── wopi │ ├── discovery.go │ ├── discovery_test.go │ ├── types.go │ ├── utf7.go │ └── wopi.go ├── routers ├── controllers │ ├── admin.go │ ├── callback.go │ ├── directory.go │ ├── file.go │ ├── main.go │ ├── share.go │ ├── site.go │ ├── slave.go │ ├── user.go │ ├── webdav.go │ ├── wopi.go │ └── workflow.go └── router.go └── service ├── admin ├── file.go ├── group.go ├── list.go ├── node.go ├── policy.go ├── response.go ├── share.go ├── site.go ├── task.go ├── tools.go └── user.go ├── basic └── site.go ├── callback ├── oauth.go └── upload.go ├── explorer ├── entity.go ├── file.go ├── metadata.go ├── pin.go ├── response.go ├── slave.go ├── upload.go ├── viewer.go └── workflows.go ├── node ├── response.go ├── rpc.go └── task.go ├── setting ├── response.go └── webdav.go ├── share ├── manage.go ├── response.go └── visit.go └── user ├── info.go ├── login.go ├── passkey.go ├── register.go ├── response.go └── setting.go /.build/aria2.supervisor.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=false 3 | 4 | [program:background_process] 5 | command=aria2c --enable-rpc --save-session /cloudreve/data 6 | autostart=true 7 | autorestart=true -------------------------------------------------------------------------------- /.build/build-assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | export NODE_OPTIONS="--max-old-space-size=8192" 4 | 5 | # This script is used to build the assets for the application. 6 | cd assets 7 | rm -rf build 8 | yarn install --network-timeout 1000000 9 | yarn version --new-version $1 --no-git-tag-version 10 | yarn run build 11 | 12 | # Copy the build files to the application directory 13 | cd ../ 14 | zip -r - assets/build >assets.zip 15 | mv assets.zip application/statics -------------------------------------------------------------------------------- /.build/entrypoint.sh: -------------------------------------------------------------------------------- 1 | supervisord -c ./aria2.supervisor.conf 2 | ./cloudreve -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://cloudreve.org/pricing"] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 360 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 30 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - pinned 16 | - security 17 | - "[Status] Maybe Later" 18 | 19 | # Set to true to ignore issues in a project (defaults to false) 20 | exemptProjects: true 21 | 22 | # Set to true to ignore issues in a milestone (defaults to false) 23 | exemptMilestones: true 24 | 25 | # Set to true to ignore issues with an assignee (defaults to false) 26 | exemptAssignees: true 27 | 28 | # Label to use when marking as stale 29 | staleLabel: wontfix 30 | 31 | # Comment to post when marking as stale. Set to `false` to disable 32 | markComment: > 33 | This issue has been automatically marked as stale because it has not had 34 | recent activity. It will be closed if no further activity occurs. Thank you 35 | for your contributions. 36 | 37 | # Comment to post when removing the stale label. 38 | # unmarkComment: > 39 | # Your comment here. 40 | 41 | # Comment to post when closing a stale Issue or Pull Request. 42 | # closeComment: > 43 | # Your comment here. 44 | 45 | # Limit the number of actions per hour, from 1-30. Default is 30 46 | limitPerRun: 30 47 | 48 | # Limit to only `issues` or `pulls` 49 | # only: issues 50 | 51 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 52 | # pulls: 53 | # daysUntilStale: 30 54 | # markComment: > 55 | # This pull request has been automatically marked as stale because it has not had 56 | # recent activity. It will be closed if no further activity occurs. Thank you 57 | # for your contributions. 58 | 59 | # issues: 60 | # exemptLabels: 61 | # - confirmed 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.db 8 | *.bin 9 | /release/ 10 | application/statics/assets.zip 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Development enviroment 19 | .idea/* 20 | uploads/* 21 | temp 22 | 23 | # Version control 24 | version.lock 25 | 26 | # Config file 27 | *.ini 28 | conf/conf.ini 29 | /statik/ 30 | .vscode/ 31 | 32 | dist/ 33 | data/ 34 | tmp/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "assets"] 2 | path = assets 3 | url = https://github.com/cloudreve/frontend.git 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - chmod +x ./.build/build-assets.sh 7 | - ./.build/build-assets.sh {{.Version}} 8 | 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | 13 | binary: cloudreve 14 | 15 | ldflags: 16 | - -s -w 17 | - -X 'github.com/cloudreve/Cloudreve/v4/application/constants.BackendVersion={{.Tag}}' -X 'github.com/cloudreve/Cloudreve/v4/application/constants.LastCommit={{.ShortCommit}}' 18 | 19 | goos: 20 | - linux 21 | - windows 22 | - darwin 23 | 24 | goarch: 25 | - amd64 26 | - arm 27 | - arm64 28 | 29 | goarm: 30 | - 5 31 | - 6 32 | - 7 33 | 34 | ignore: 35 | - goos: windows 36 | goarm: 5 37 | - goos: windows 38 | goarm: 6 39 | - goos: windows 40 | goarm: 7 41 | 42 | archives: 43 | - formats: ["tar.gz"] 44 | # this name template makes the OS and Arch compatible with the results of uname. 45 | name_template: >- 46 | cloudreve_{{.Tag}}_{{- .Os }}_{{ .Arch }} 47 | {{- if .Arm }}v{{ .Arm }}{{ end }} 48 | # use zip for windows archives 49 | format_overrides: 50 | - goos: windows 51 | formats: ["zip"] 52 | 53 | checksum: 54 | name_template: "checksums.txt" 55 | snapshot: 56 | version_template: "{{ incpatch .Version }}-next" 57 | 58 | changelog: 59 | sort: asc 60 | filters: 61 | exclude: 62 | - "^docs:" 63 | - "^test:" 64 | 65 | release: 66 | draft: true 67 | prerelease: auto 68 | target_commitish: "{{ .Commit }}" 69 | name_template: "{{.Version}}" 70 | 71 | dockers: 72 | - dockerfile: Dockerfile 73 | use: buildx 74 | build_flag_templates: 75 | - "--platform=linux/amd64" 76 | goos: linux 77 | goarch: amd64 78 | goamd64: v1 79 | extra_files: 80 | - .build/aria2.supervisor.conf 81 | - .build/entrypoint.sh 82 | image_templates: 83 | - "cloudreve/cloudreve:{{ .Tag }}-amd64" 84 | - dockerfile: Dockerfile 85 | use: buildx 86 | build_flag_templates: 87 | - "--platform=linux/arm64" 88 | goos: linux 89 | goarch: arm64 90 | extra_files: 91 | - .build/aria2.supervisor.conf 92 | - .build/entrypoint.sh 93 | image_templates: 94 | - "cloudreve/cloudreve:{{ .Tag }}-arm64" 95 | 96 | docker_manifests: 97 | - name_template: "cloudreve/cloudreve:latest" 98 | image_templates: 99 | - "cloudreve/cloudreve:{{ .Tag }}-amd64" 100 | - "cloudreve/cloudreve:{{ .Tag }}-arm64" 101 | - name_template: "cloudreve/cloudreve:{{ .Tag }}" 102 | image_templates: 103 | - "cloudreve/cloudreve:{{ .Tag }}-amd64" 104 | - "cloudreve/cloudreve:{{ .Tag }}-arm64" 105 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | WORKDIR /cloudreve 4 | 5 | RUN apk update \ 6 | && apk add --no-cache tzdata vips-tools ffmpeg libreoffice aria2 supervisor font-noto font-noto-cjk \ 7 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 8 | && echo "Asia/Shanghai" > /etc/timezone \ 9 | && mkdir -p ./data/temp/aria2 \ 10 | && chmod -R 766 ./data/temp/aria2 11 | 12 | ENV CR_ENABLE_ARIA2=1 \ 13 | CR_SETTING_DEFAULT_thumb_ffmpeg_enabled=1 \ 14 | CR_SETTING_DEFAULT_thumb_vips_enabled=1 \ 15 | CR_SETTING_DEFAULT_thumb_libreoffice_enabled=1 \ 16 | CR_SETTING_DEFAULT_media_meta_ffprobe=1 17 | 18 | COPY .build/aria2.supervisor.conf .build/entrypoint.sh ./ 19 | COPY cloudreve ./cloudreve 20 | 21 | RUN chmod +x ./cloudreve \ 22 | && chmod +x ./entrypoint.sh 23 | 24 | EXPOSE 5212 443 25 | 26 | VOLUME ["/cloudreve/data"] 27 | 28 | ENTRYPOINT ["sh", "./entrypoint.sh"] 29 | 30 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | * For security issues with high-impacts (e.g. related to payments or user permission), we support 3.8.x and all 4.x version. But the fix for 4.x will released only in latest sub-version. 6 | * For all other security issues, we mainly support version >= 4.x (in which `x` is the latest stable sub-version). 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Please send the details about the security issue to `support@cloudreve.org`. Once the vulnerability is comfirmed or fixed, you will get updates from the email thread. 11 | 12 | We will reward you with bounty/swag for success submission of securty issues. 13 | -------------------------------------------------------------------------------- /application/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // These values will be injected at build time, DO NOT EDIT. 4 | 5 | // BackendVersion 当前后端版本号 6 | var BackendVersion = "4.0.0-alpha.1" 7 | 8 | // IsPro 是否为Pro版本 9 | var IsPro = "false" 10 | 11 | var IsProBool = IsPro == "true" 12 | 13 | // LastCommit 最后commit id 14 | var LastCommit = "000000" 15 | 16 | const ( 17 | APIPrefix = "/api/v4" 18 | APIPrefixSlave = "/api/v4/slave" 19 | CrHeaderPrefix = "X-Cr-" 20 | ) 21 | 22 | const CloudreveScheme = "cloudreve" 23 | 24 | type ( 25 | FileSystemType string 26 | ) 27 | 28 | const ( 29 | FileSystemMy = FileSystemType("my") 30 | FileSystemShare = FileSystemType("share") 31 | FileSystemTrash = FileSystemType("trash") 32 | FileSystemSharedWithMe = FileSystemType("shared_with_me") 33 | FileSystemUnknown = FileSystemType("unknown") 34 | ) 35 | -------------------------------------------------------------------------------- /application/constants/size.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | MB = 1 << 20 5 | GB = 1 << 30 6 | TB = 1 << 40 7 | PB = 1 << 50 8 | ) 9 | -------------------------------------------------------------------------------- /application/migrator/avatars.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 10 | ) 11 | 12 | func migrateAvatars(m *Migrator) error { 13 | m.l.Info("Migrating avatars files...") 14 | avatarRoot := util.RelativePath(m.state.V3AvatarPath) 15 | 16 | for uid, _ := range m.state.UserIDs { 17 | avatarPath := filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d_2.png", uid)) 18 | 19 | // check if file exists 20 | if util.Exists(avatarPath) { 21 | m.l.Info("Migrating avatar for user %d", uid) 22 | // Copy to v4 avatar path 23 | v4Path := filepath.Join(util.DataPath("avatar"), fmt.Sprintf("avatar_%d.png", uid)) 24 | 25 | // copy 26 | origin, err := os.Open(avatarPath) 27 | if err != nil { 28 | return fmt.Errorf("failed to open avatar file: %w", err) 29 | } 30 | defer origin.Close() 31 | 32 | dest, err := util.CreatNestedFile(v4Path) 33 | if err != nil { 34 | return fmt.Errorf("failed to create avatar file: %w", err) 35 | } 36 | defer dest.Close() 37 | 38 | _, err = io.Copy(dest, origin) 39 | 40 | if err != nil { 41 | m.l.Warning("Failed to copy avatar file: %s, skipping...", err) 42 | } 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /application/migrator/conf/defaults.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | // RedisConfig Redis服务器配置 4 | var RedisConfig = &redis{ 5 | Network: "tcp", 6 | Server: "", 7 | Password: "", 8 | DB: "0", 9 | } 10 | 11 | // DatabaseConfig 数据库配置 12 | var DatabaseConfig = &database{ 13 | Type: "UNSET", 14 | Charset: "utf8", 15 | DBFile: "cloudreve.db", 16 | Port: 3306, 17 | UnixSocket: false, 18 | } 19 | 20 | // SystemConfig 系统公用配置 21 | var SystemConfig = &system{ 22 | Debug: false, 23 | Mode: "master", 24 | Listen: ":5212", 25 | ProxyHeader: "X-Forwarded-For", 26 | } 27 | 28 | // CORSConfig 跨域配置 29 | var CORSConfig = &cors{ 30 | AllowOrigins: []string{"UNSET"}, 31 | AllowMethods: []string{"PUT", "POST", "GET", "OPTIONS"}, 32 | AllowHeaders: []string{"Cookie", "X-Cr-Policy", "Authorization", "Content-Length", "Content-Type", "X-Cr-Path", "X-Cr-FileName"}, 33 | AllowCredentials: false, 34 | ExposeHeaders: nil, 35 | SameSite: "Default", 36 | Secure: false, 37 | } 38 | 39 | // SlaveConfig 从机配置 40 | var SlaveConfig = &slave{ 41 | CallbackTimeout: 20, 42 | SignatureTTL: 60, 43 | } 44 | 45 | var SSLConfig = &ssl{ 46 | Listen: ":443", 47 | CertPath: "", 48 | KeyPath: "", 49 | } 50 | 51 | var UnixConfig = &unix{ 52 | Listen: "", 53 | } 54 | 55 | var OptionOverwrite = map[string]interface{}{} 56 | -------------------------------------------------------------------------------- /application/migrator/directlink.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/cloudreve/Cloudreve/v4/application/migrator/model" 8 | "github.com/cloudreve/Cloudreve/v4/ent/file" 9 | "github.com/cloudreve/Cloudreve/v4/pkg/conf" 10 | ) 11 | 12 | func (m *Migrator) migrateDirectLink() error { 13 | m.l.Info("Migrating direct links...") 14 | batchSize := 1000 15 | offset := m.state.DirectLinkOffset 16 | ctx := context.Background() 17 | 18 | if m.state.DirectLinkOffset > 0 { 19 | m.l.Info("Resuming direct link migration from offset %d", offset) 20 | } 21 | 22 | for { 23 | m.l.Info("Migrating direct links with offset %d", offset) 24 | var directLinks []model.SourceLink 25 | if err := model.DB.Limit(batchSize).Offset(offset).Find(&directLinks).Error; err != nil { 26 | return fmt.Errorf("failed to list v3 direct links: %w", err) 27 | } 28 | 29 | if len(directLinks) == 0 { 30 | if m.dep.ConfigProvider().Database().Type == conf.PostgresDB { 31 | m.l.Info("Resetting direct link ID sequence for postgres...") 32 | m.v4client.DirectLink.ExecContext(ctx, "SELECT SETVAL('direct_links_id_seq', (SELECT MAX(id) FROM direct_links))") 33 | } 34 | break 35 | } 36 | 37 | tx, err := m.v4client.Tx(ctx) 38 | if err != nil { 39 | _ = tx.Rollback() 40 | return fmt.Errorf("failed to start transaction: %w", err) 41 | } 42 | 43 | for _, dl := range directLinks { 44 | sourceId := int(dl.FileID) + m.state.LastFolderID 45 | // check if file exists 46 | _, err = tx.File.Query().Where(file.ID(sourceId)).First(ctx) 47 | if err != nil { 48 | m.l.Warning("File %d not found, skipping direct link %d", sourceId, dl.ID) 49 | continue 50 | } 51 | 52 | stm := tx.DirectLink.Create(). 53 | SetCreatedAt(formatTime(dl.CreatedAt)). 54 | SetUpdatedAt(formatTime(dl.UpdatedAt)). 55 | SetRawID(int(dl.ID)). 56 | SetFileID(sourceId). 57 | SetName(dl.Name). 58 | SetDownloads(dl.Downloads). 59 | SetSpeed(0) 60 | 61 | if _, err := stm.Save(ctx); err != nil { 62 | _ = tx.Rollback() 63 | return fmt.Errorf("failed to create direct link %d: %w", dl.ID, err) 64 | } 65 | } 66 | 67 | if err := tx.Commit(); err != nil { 68 | return fmt.Errorf("failed to commit transaction: %w", err) 69 | } 70 | 71 | offset += batchSize 72 | m.state.DirectLinkOffset = offset 73 | if err := m.saveState(); err != nil { 74 | m.l.Warning("Failed to save state after direct link batch: %s", err) 75 | } else { 76 | m.l.Info("Saved migration state after processing this batch") 77 | } 78 | } 79 | 80 | return nil 81 | 82 | } 83 | -------------------------------------------------------------------------------- /application/migrator/model/file.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // File 文件 8 | type File struct { 9 | // 表字段 10 | gorm.Model 11 | Name string `gorm:"unique_index:idx_only_one"` 12 | SourceName string `gorm:"type:text"` 13 | UserID uint `gorm:"index:user_id;unique_index:idx_only_one"` 14 | Size uint64 15 | PicInfo string 16 | FolderID uint `gorm:"index:folder_id;unique_index:idx_only_one"` 17 | PolicyID uint 18 | UploadSessionID *string `gorm:"index:session_id;unique_index:session_only_one"` 19 | Metadata string `gorm:"type:text"` 20 | 21 | // 关联模型 22 | Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"` 23 | 24 | // 数据库忽略字段 25 | Position string `gorm:"-"` 26 | MetadataSerialized map[string]string `gorm:"-"` 27 | } 28 | 29 | // Thumb related metadata 30 | const ( 31 | ThumbStatusNotExist = "" 32 | ThumbStatusExist = "exist" 33 | ThumbStatusNotAvailable = "not_available" 34 | 35 | ThumbStatusMetadataKey = "thumb_status" 36 | ThumbSidecarMetadataKey = "thumb_sidecar" 37 | 38 | ChecksumMetadataKey = "webdav_checksum" 39 | ) 40 | -------------------------------------------------------------------------------- /application/migrator/model/folder.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Folder 目录 8 | type Folder struct { 9 | // 表字段 10 | gorm.Model 11 | Name string `gorm:"unique_index:idx_only_one_name"` 12 | ParentID *uint `gorm:"index:parent_id;unique_index:idx_only_one_name"` 13 | OwnerID uint `gorm:"index:owner_id"` 14 | 15 | // 数据库忽略字段 16 | Position string `gorm:"-"` 17 | WebdavDstName string `gorm:"-"` 18 | } 19 | -------------------------------------------------------------------------------- /application/migrator/model/group.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Group 用户组模型 8 | type Group struct { 9 | gorm.Model 10 | Name string 11 | Policies string 12 | MaxStorage uint64 13 | ShareEnabled bool 14 | WebDAVEnabled bool 15 | SpeedLimit int 16 | Options string `json:"-" gorm:"size:4294967295"` 17 | 18 | // 数据库忽略字段 19 | PolicyList []uint `gorm:"-"` 20 | OptionsSerialized GroupOption `gorm:"-"` 21 | } 22 | 23 | // GroupOption 用户组其他配置 24 | type GroupOption struct { 25 | ArchiveDownload bool `json:"archive_download,omitempty"` // 打包下载 26 | ArchiveTask bool `json:"archive_task,omitempty"` // 在线压缩 27 | CompressSize uint64 `json:"compress_size,omitempty"` // 可压缩大小 28 | DecompressSize uint64 `json:"decompress_size,omitempty"` 29 | OneTimeDownload bool `json:"one_time_download,omitempty"` 30 | ShareDownload bool `json:"share_download,omitempty"` 31 | Aria2 bool `json:"aria2,omitempty"` // 离线下载 32 | Aria2Options map[string]interface{} `json:"aria2_options,omitempty"` // 离线下载用户组配置 33 | SourceBatchSize int `json:"source_batch,omitempty"` 34 | RedirectedSource bool `json:"redirected_source,omitempty"` 35 | Aria2BatchSize int `json:"aria2_batch,omitempty"` 36 | AdvanceDelete bool `json:"advance_delete,omitempty"` 37 | WebDAVProxy bool `json:"webdav_proxy,omitempty"` 38 | } 39 | -------------------------------------------------------------------------------- /application/migrator/model/init.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/jinzhu/gorm" 8 | 9 | "github.com/cloudreve/Cloudreve/v4/application/migrator/conf" 10 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 11 | _ "github.com/jinzhu/gorm/dialects/mssql" 12 | _ "github.com/jinzhu/gorm/dialects/mysql" 13 | _ "github.com/jinzhu/gorm/dialects/postgres" 14 | ) 15 | 16 | // DB 数据库链接单例 17 | var DB *gorm.DB 18 | 19 | // Init 初始化 MySQL 链接 20 | func Init() error { 21 | var ( 22 | db *gorm.DB 23 | err error 24 | confDBType string = conf.DatabaseConfig.Type 25 | ) 26 | 27 | // 兼容已有配置中的 "sqlite3" 配置项 28 | if confDBType == "sqlite3" { 29 | confDBType = "sqlite" 30 | } 31 | 32 | switch confDBType { 33 | case "UNSET", "sqlite": 34 | // 未指定数据库或者明确指定为 sqlite 时,使用 SQLite 数据库 35 | db, err = gorm.Open("sqlite3", util.RelativePath(conf.DatabaseConfig.DBFile)) 36 | case "postgres": 37 | db, err = gorm.Open(confDBType, fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", 38 | conf.DatabaseConfig.Host, 39 | conf.DatabaseConfig.User, 40 | conf.DatabaseConfig.Password, 41 | conf.DatabaseConfig.Name, 42 | conf.DatabaseConfig.Port)) 43 | case "mysql", "mssql": 44 | var host string 45 | if conf.DatabaseConfig.UnixSocket { 46 | host = fmt.Sprintf("unix(%s)", 47 | conf.DatabaseConfig.Host) 48 | } else { 49 | host = fmt.Sprintf("(%s:%d)", 50 | conf.DatabaseConfig.Host, 51 | conf.DatabaseConfig.Port) 52 | } 53 | 54 | db, err = gorm.Open(confDBType, fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=True&loc=Local", 55 | conf.DatabaseConfig.User, 56 | conf.DatabaseConfig.Password, 57 | host, 58 | conf.DatabaseConfig.Name, 59 | conf.DatabaseConfig.Charset)) 60 | default: 61 | return fmt.Errorf("unsupported database type %q", confDBType) 62 | } 63 | 64 | //db.SetLogger(util.Log()) 65 | if err != nil { 66 | return fmt.Errorf("failed to connect to database: %w", err) 67 | } 68 | 69 | // 处理表前缀 70 | gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string { 71 | return conf.DatabaseConfig.TablePrefix + defaultTableName 72 | } 73 | 74 | // Debug模式下,输出所有 SQL 日志 75 | db.LogMode(true) 76 | 77 | //设置连接池 78 | db.DB().SetMaxIdleConns(50) 79 | if confDBType == "sqlite" || confDBType == "UNSET" { 80 | db.DB().SetMaxOpenConns(1) 81 | } else { 82 | db.DB().SetMaxOpenConns(100) 83 | } 84 | 85 | //超时 86 | db.DB().SetConnMaxLifetime(time.Second * 30) 87 | 88 | DB = db 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /application/migrator/model/node.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Node 从机节点信息模型 8 | type Node struct { 9 | gorm.Model 10 | Status NodeStatus // 节点状态 11 | Name string // 节点别名 12 | Type ModelType // 节点状态 13 | Server string // 服务器地址 14 | SlaveKey string `gorm:"type:text"` // 主->从 通信密钥 15 | MasterKey string `gorm:"type:text"` // 从->主 通信密钥 16 | Aria2Enabled bool // 是否支持用作离线下载节点 17 | Aria2Options string `gorm:"type:text"` // 离线下载配置 18 | Rank int // 负载均衡权重 19 | 20 | // 数据库忽略字段 21 | Aria2OptionsSerialized Aria2Option `gorm:"-"` 22 | } 23 | 24 | // Aria2Option 非公有的Aria2配置属性 25 | type Aria2Option struct { 26 | // RPC 服务器地址 27 | Server string `json:"server,omitempty"` 28 | // RPC 密钥 29 | Token string `json:"token,omitempty"` 30 | // 临时下载目录 31 | TempPath string `json:"temp_path,omitempty"` 32 | // 附加下载配置 33 | Options string `json:"options,omitempty"` 34 | // 下载监控间隔 35 | Interval int `json:"interval,omitempty"` 36 | // RPC API 请求超时 37 | Timeout int `json:"timeout,omitempty"` 38 | } 39 | 40 | type NodeStatus int 41 | type ModelType int 42 | 43 | const ( 44 | NodeActive NodeStatus = iota 45 | NodeSuspend 46 | ) 47 | 48 | const ( 49 | SlaveNodeType ModelType = iota 50 | MasterNodeType 51 | ) 52 | -------------------------------------------------------------------------------- /application/migrator/model/policy.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Policy 存储策略 8 | type Policy struct { 9 | // 表字段 10 | gorm.Model 11 | Name string 12 | Type string 13 | Server string 14 | BucketName string 15 | IsPrivate bool 16 | BaseURL string 17 | AccessKey string `gorm:"type:text"` 18 | SecretKey string `gorm:"type:text"` 19 | MaxSize uint64 20 | AutoRename bool 21 | DirNameRule string 22 | FileNameRule string 23 | IsOriginLinkEnable bool 24 | Options string `gorm:"type:text"` 25 | 26 | // 数据库忽略字段 27 | OptionsSerialized PolicyOption `gorm:"-"` 28 | MasterID string `gorm:"-"` 29 | } 30 | 31 | // PolicyOption 非公有的存储策略属性 32 | type PolicyOption struct { 33 | // Upyun访问Token 34 | Token string `json:"token"` 35 | // 允许的文件扩展名 36 | FileType []string `json:"file_type"` 37 | // MimeType 38 | MimeType string `json:"mimetype"` 39 | // OauthRedirect Oauth 重定向地址 40 | OauthRedirect string `json:"od_redirect,omitempty"` 41 | // OdProxy Onedrive 反代地址 42 | OdProxy string `json:"od_proxy,omitempty"` 43 | // OdDriver OneDrive 驱动器定位符 44 | OdDriver string `json:"od_driver,omitempty"` 45 | // Region 区域代码 46 | Region string `json:"region,omitempty"` 47 | // ServerSideEndpoint 服务端请求使用的 Endpoint,为空时使用 Policy.Server 字段 48 | ServerSideEndpoint string `json:"server_side_endpoint,omitempty"` 49 | // 分片上传的分片大小 50 | ChunkSize uint64 `json:"chunk_size,omitempty"` 51 | // 分片上传时是否需要预留空间 52 | PlaceholderWithSize bool `json:"placeholder_with_size,omitempty"` 53 | // 每秒对存储端的 API 请求上限 54 | TPSLimit float64 `json:"tps_limit,omitempty"` 55 | // 每秒 API 请求爆发上限 56 | TPSLimitBurst int `json:"tps_limit_burst,omitempty"` 57 | // Set this to `true` to force the request to use path-style addressing, 58 | // i.e., `http://s3.amazonaws.com/BUCKET/KEY ` 59 | S3ForcePathStyle bool `json:"s3_path_style"` 60 | // File extensions that support thumbnail generation using native policy API. 61 | ThumbExts []string `json:"thumb_exts,omitempty"` 62 | } 63 | -------------------------------------------------------------------------------- /application/migrator/model/setting.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Setting 系统设置模型 8 | type Setting struct { 9 | gorm.Model 10 | Type string `gorm:"not null"` 11 | Name string `gorm:"unique;not null;index:setting_key"` 12 | Value string `gorm:"size:65535"` 13 | } 14 | -------------------------------------------------------------------------------- /application/migrator/model/share.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | // Share 分享模型 10 | type Share struct { 11 | gorm.Model 12 | Password string // 分享密码,空值为非加密分享 13 | IsDir bool // 原始资源是否为目录 14 | UserID uint // 创建用户ID 15 | SourceID uint // 原始资源ID 16 | Views int // 浏览数 17 | Downloads int // 下载数 18 | RemainDownloads int // 剩余下载配额,负值标识无限制 19 | Expires *time.Time // 过期时间,空值表示无过期时间 20 | PreviewEnabled bool // 是否允许直接预览 21 | SourceName string `gorm:"index:source"` // 用于搜索的字段 22 | 23 | // 数据库忽略字段 24 | User User `gorm:"PRELOAD:false,association_autoupdate:false"` 25 | File File `gorm:"PRELOAD:false,association_autoupdate:false"` 26 | Folder Folder `gorm:"PRELOAD:false,association_autoupdate:false"` 27 | } 28 | -------------------------------------------------------------------------------- /application/migrator/model/source_link.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // SourceLink represent a shared file source link 8 | type SourceLink struct { 9 | gorm.Model 10 | FileID uint // corresponding file ID 11 | Name string // name of the file while creating the source link, for annotation 12 | Downloads int // 下载数 13 | 14 | // 关联模型 15 | File File `gorm:"save_associations:false:false"` 16 | } 17 | -------------------------------------------------------------------------------- /application/migrator/model/tag.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Tag 用户自定义标签 8 | type Tag struct { 9 | gorm.Model 10 | Name string // 标签名 11 | Icon string // 图标标识 12 | Color string // 图标颜色 13 | Type int // 标签类型(文件分类/目录直达) 14 | Expression string `gorm:"type:text"` // 搜索表表达式/直达路径 15 | UserID uint // 创建者ID 16 | } 17 | 18 | const ( 19 | // FileTagType 文件分类标签 20 | FileTagType = iota 21 | // DirectoryLinkType 目录快捷方式标签 22 | DirectoryLinkType 23 | ) 24 | -------------------------------------------------------------------------------- /application/migrator/model/task.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Task 任务模型 8 | type Task struct { 9 | gorm.Model 10 | Status int // 任务状态 11 | Type int // 任务类型 12 | UserID uint // 发起者UID,0表示为系统发起 13 | Progress int // 进度 14 | Error string `gorm:"type:text"` // 错误信息 15 | Props string `gorm:"type:text"` // 任务属性 16 | } 17 | -------------------------------------------------------------------------------- /application/migrator/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | const ( 8 | // Active 账户正常状态 9 | Active = iota 10 | // NotActivicated 未激活 11 | NotActivicated 12 | // Baned 被封禁 13 | Baned 14 | // OveruseBaned 超额使用被封禁 15 | OveruseBaned 16 | ) 17 | 18 | // User 用户模型 19 | type User struct { 20 | // 表字段 21 | gorm.Model 22 | Email string `gorm:"type:varchar(100);unique_index"` 23 | Nick string `gorm:"size:50"` 24 | Password string `json:"-"` 25 | Status int 26 | GroupID uint 27 | Storage uint64 28 | TwoFactor string 29 | Avatar string 30 | Options string `json:"-" gorm:"size:4294967295"` 31 | Authn string `gorm:"size:4294967295"` 32 | 33 | // 关联模型 34 | Group Group `gorm:"save_associations:false:false"` 35 | Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"` 36 | 37 | // 数据库忽略字段 38 | OptionsSerialized UserOption `gorm:"-"` 39 | } 40 | 41 | // UserOption 用户个性化配置字段 42 | type UserOption struct { 43 | ProfileOff bool `json:"profile_off,omitempty"` 44 | PreferredTheme string `json:"preferred_theme,omitempty"` 45 | } 46 | -------------------------------------------------------------------------------- /application/migrator/model/webdav.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // Webdav 应用账户 8 | type Webdav struct { 9 | gorm.Model 10 | Name string // 应用名称 11 | Password string `gorm:"unique_index:password_only_on"` // 应用密码 12 | UserID uint `gorm:"unique_index:password_only_on"` // 用户ID 13 | Root string `gorm:"type:text"` // 根目录 14 | Readonly bool `gorm:"type:bool"` // 是否只读 15 | UseProxy bool `gorm:"type:bool"` // 是否进行反代 16 | } 17 | -------------------------------------------------------------------------------- /application/migrator/node.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/cloudreve/Cloudreve/v4/application/migrator/model" 9 | "github.com/cloudreve/Cloudreve/v4/ent/node" 10 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 11 | "github.com/cloudreve/Cloudreve/v4/pkg/boolset" 12 | ) 13 | 14 | func (m *Migrator) migrateNode() error { 15 | m.l.Info("Migrating nodes...") 16 | 17 | var nodes []model.Node 18 | if err := model.DB.Find(&nodes).Error; err != nil { 19 | return fmt.Errorf("failed to list v3 nodes: %w", err) 20 | } 21 | 22 | for _, n := range nodes { 23 | nodeType := node.TypeSlave 24 | nodeStatus := node.StatusSuspended 25 | if n.Type == model.MasterNodeType { 26 | nodeType = node.TypeMaster 27 | } 28 | if n.Status == model.NodeActive { 29 | nodeStatus = node.StatusActive 30 | } 31 | 32 | cap := &boolset.BooleanSet{} 33 | settings := &types.NodeSetting{ 34 | Provider: types.DownloaderProviderAria2, 35 | } 36 | 37 | if n.Aria2Enabled { 38 | boolset.Sets(map[types.NodeCapability]bool{ 39 | types.NodeCapabilityRemoteDownload: true, 40 | }, cap) 41 | 42 | aria2Options := &model.Aria2Option{} 43 | if err := json.Unmarshal([]byte(n.Aria2Options), aria2Options); err != nil { 44 | return fmt.Errorf("failed to unmarshal aria2 options: %w", err) 45 | } 46 | 47 | downloaderOptions := map[string]any{} 48 | if aria2Options.Options != "" { 49 | if err := json.Unmarshal([]byte(aria2Options.Options), &downloaderOptions); err != nil { 50 | return fmt.Errorf("failed to unmarshal aria2 options: %w", err) 51 | } 52 | } 53 | 54 | settings.Aria2Setting = &types.Aria2Setting{ 55 | Server: aria2Options.Server, 56 | Token: aria2Options.Token, 57 | Options: downloaderOptions, 58 | TempPath: aria2Options.TempPath, 59 | } 60 | } 61 | 62 | if n.Type == model.MasterNodeType { 63 | boolset.Sets(map[types.NodeCapability]bool{ 64 | types.NodeCapabilityExtractArchive: true, 65 | types.NodeCapabilityCreateArchive: true, 66 | }, cap) 67 | } 68 | 69 | stm := m.v4client.Node.Create(). 70 | SetRawID(int(n.ID)). 71 | SetCreatedAt(formatTime(n.CreatedAt)). 72 | SetUpdatedAt(formatTime(n.UpdatedAt)). 73 | SetName(n.Name). 74 | SetType(nodeType). 75 | SetStatus(nodeStatus). 76 | SetServer(n.Server). 77 | SetSlaveKey(n.SlaveKey). 78 | SetCapabilities(cap). 79 | SetSettings(settings). 80 | SetWeight(n.Rank) 81 | 82 | if err := stm.Exec(context.Background()); err != nil { 83 | return fmt.Errorf("failed to create node %q: %w", n.Name, err) 84 | } 85 | 86 | } 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | tags: 3 | include: 4 | - '*' 5 | variables: 6 | GO_VERSION: "1.23.6" 7 | NODE_VERSION: "22.x" 8 | DOCKER_BUILDKIT: 1 9 | 10 | pool: 11 | vmImage: ubuntu-latest 12 | 13 | jobs: 14 | - job: Release 15 | steps: 16 | - checkout: self 17 | submodules: true 18 | persistCredentials: true 19 | - task: NodeTool@0 20 | inputs: 21 | versionSpec: '$(NODE_VERSION)' 22 | displayName: 'Install Node.js' 23 | - task: GoTool@0 24 | inputs: 25 | version: "$(GO_VERSION)" 26 | displayName: Install Go 27 | - task: Docker@2 28 | inputs: 29 | containerRegistry: "CR DockerHub" 30 | command: "login" 31 | addPipelineData: false 32 | addBaseImageData: false 33 | - task: CmdLine@2 34 | displayName: "Install multiarch/qemu-user-static" 35 | inputs: 36 | script: | 37 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 38 | - task: goreleaser@0 39 | condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/')) 40 | inputs: 41 | version: "latest" 42 | distribution: "goreleaser" 43 | workdir: "$(Build.SourcesDirectory)" 44 | args: "release --timeout 60m" 45 | env: 46 | GITHUB_TOKEN: $(GITHUB_TOKEN) 47 | -------------------------------------------------------------------------------- /cmd/eject.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/application/constants" 5 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 6 | "github.com/cloudreve/Cloudreve/v4/application/statics" 7 | "github.com/spf13/cobra" 8 | "os" 9 | ) 10 | 11 | func init() { 12 | rootCmd.AddCommand(ejectCmd) 13 | } 14 | 15 | var ejectCmd = &cobra.Command{ 16 | Use: "eject", 17 | Short: "Eject all embedded static files", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | dep := dependency.NewDependency( 20 | dependency.WithConfigPath(confPath), 21 | dependency.WithProFlag(constants.IsPro == "true"), 22 | ) 23 | logger := dep.Logger() 24 | 25 | if err := statics.Eject(dep.Logger(), dep.Statics()); err != nil { 26 | logger.Error("Failed to eject static files: %s", err) 27 | os.Exit(1) 28 | } 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /cmd/migrate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/cloudreve/Cloudreve/v4/application/constants" 8 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 9 | "github.com/cloudreve/Cloudreve/v4/application/migrator" 10 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | v3ConfPath string 16 | forceReset bool 17 | ) 18 | 19 | func init() { 20 | rootCmd.AddCommand(migrateCmd) 21 | migrateCmd.PersistentFlags().StringVar(&v3ConfPath, "v3-conf", "", "Path to the v3 config file") 22 | migrateCmd.PersistentFlags().BoolVar(&forceReset, "force-reset", false, "Force reset migration state and start from beginning") 23 | } 24 | 25 | var migrateCmd = &cobra.Command{ 26 | Use: "migrate", 27 | Short: "Migrate from v3 to v4", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | dep := dependency.NewDependency( 30 | dependency.WithConfigPath(confPath), 31 | dependency.WithRequiredDbVersion(constants.BackendVersion), 32 | dependency.WithProFlag(constants.IsPro == "true"), 33 | ) 34 | logger := dep.Logger() 35 | logger.Info("Migrating from v3 to v4...") 36 | 37 | if v3ConfPath == "" { 38 | logger.Error("v3 config file is required, please use -v3-conf to specify the path.") 39 | os.Exit(1) 40 | } 41 | 42 | // Check if state file exists and warn about resuming 43 | stateFilePath := filepath.Join(filepath.Dir(v3ConfPath), "migration_state.json") 44 | if util.Exists(stateFilePath) && !forceReset { 45 | logger.Info("Found existing migration state file at %s. Migration will resume from the last successful step.", stateFilePath) 46 | logger.Info("If you want to start migration from the beginning, please use --force-reset flag.") 47 | } else if forceReset && util.Exists(stateFilePath) { 48 | logger.Info("Force resetting migration state. Will start from the beginning.") 49 | if err := os.Remove(stateFilePath); err != nil { 50 | logger.Error("Failed to remove migration state file: %s", err) 51 | os.Exit(1) 52 | } 53 | } 54 | 55 | migrator, err := migrator.NewMigrator(dep, v3ConfPath) 56 | if err != nil { 57 | logger.Error("Failed to create migrator: %s", err) 58 | os.Exit(1) 59 | } 60 | 61 | if err := migrator.Migrate(); err != nil { 62 | logger.Error("Failed to migrate: %s", err) 63 | logger.Info("Migration failed but state has been saved. You can retry with the same command to resume from the last successful step.") 64 | os.Exit(1) 65 | } 66 | 67 | logger.Info("Migration from v3 to v4 completed successfully.") 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | "os" 9 | ) 10 | 11 | var ( 12 | confPath string 13 | ) 14 | 15 | func init() { 16 | rootCmd.PersistentFlags().StringVarP(&confPath, "conf", "c", util.DataPath("conf.ini"), "Path to the config file") 17 | rootCmd.PersistentFlags().BoolVarP(&util.UseWorkingDir, "use-working-dir", "w", false, "Use working directory, instead of executable directory") 18 | } 19 | 20 | var rootCmd = &cobra.Command{ 21 | Use: "cloudreve", 22 | Short: "Cloudreve is a server-side self-hosted cloud storage platform", 23 | Long: `Self-hosted file management and sharing system, supports multiple storage providers. 24 | Complete documentation is available at https://docs.cloudreve.org/`, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | // Do Stuff Here 27 | }, 28 | } 29 | 30 | func Execute() { 31 | cmd, _, err := rootCmd.Find(os.Args[1:]) 32 | // redirect to default server cmd if no cmd is given 33 | if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp { 34 | args := append([]string{"server"}, os.Args[1:]...) 35 | rootCmd.SetArgs(args) 36 | } 37 | 38 | if err := rootCmd.Execute(); err != nil { 39 | fmt.Println(err) 40 | os.Exit(1) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/cloudreve/Cloudreve/v4/application" 9 | "github.com/cloudreve/Cloudreve/v4/application/constants" 10 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 11 | "github.com/cloudreve/Cloudreve/v4/pkg/logging" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | licenseKey string 17 | ) 18 | 19 | func init() { 20 | rootCmd.AddCommand(serverCmd) 21 | serverCmd.PersistentFlags().StringVarP(&licenseKey, "license-key", "l", "", "License key of your Cloudreve Pro") 22 | } 23 | 24 | var serverCmd = &cobra.Command{ 25 | Use: "server", 26 | Short: "Start a Cloudreve server with the given config file", 27 | Run: func(cmd *cobra.Command, args []string) { 28 | dep := dependency.NewDependency( 29 | dependency.WithConfigPath(confPath), 30 | dependency.WithProFlag(constants.IsProBool), 31 | dependency.WithRequiredDbVersion(constants.BackendVersion), 32 | dependency.WithLicenseKey(licenseKey), 33 | ) 34 | server := application.NewServer(dep) 35 | logger := dep.Logger() 36 | 37 | server.PrintBanner() 38 | 39 | // Graceful shutdown after received signal. 40 | sigChan := make(chan os.Signal, 1) 41 | signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) 42 | go shutdown(sigChan, logger, server) 43 | 44 | if err := server.Start(); err != nil { 45 | logger.Error("Failed to start server: %s", err) 46 | os.Exit(1) 47 | } 48 | 49 | defer func() { 50 | <-sigChan 51 | }() 52 | }, 53 | } 54 | 55 | func shutdown(sigChan chan os.Signal, logger logging.Logger, server application.Server) { 56 | sig := <-sigChan 57 | logger.Info("Signal %s received, shutting down server...", sig) 58 | server.Close() 59 | close(sigChan) 60 | } 61 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pro: 3 | image: cloudreve/cloudreve:latest 4 | container_name: cloudreve-backend 5 | depends_on: 6 | - postgresql 7 | - redis 8 | restart: always 9 | ports: 10 | - 5212:5212 11 | environment: 12 | - CR_CONF_Database.Type=postgres 13 | - CR_CONF_Database.Host=postgresql 14 | - CR_CONF_Database.User=cloudreve 15 | - CR_CONF_Database.Name=cloudreve 16 | - CR_CONF_Database.Port=5432 17 | - CR_CONF_Redis.Server=redis:6379 18 | volumes: 19 | - backend_data:/cloudreve/data 20 | 21 | postgresql: 22 | image: postgres:latest 23 | container_name: postgresql 24 | environment: 25 | - POSTGRES_USER=cloudreve 26 | - POSTGRES_DB=cloudreve 27 | - POSTGRES_HOST_AUTH_METHOD=trust 28 | volumes: 29 | - database_postgres:/var/lib/postgresql/data 30 | 31 | redis: 32 | image: redis:latest 33 | container_name: redis 34 | volumes: 35 | - backend_data:/data 36 | 37 | volumes: 38 | backend_data: 39 | database_postgres: 40 | -------------------------------------------------------------------------------- /ent/davaccount_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/davaccount" 12 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 13 | ) 14 | 15 | // DavAccountDelete is the builder for deleting a DavAccount entity. 16 | type DavAccountDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *DavAccountMutation 20 | } 21 | 22 | // Where appends a list predicates to the DavAccountDelete builder. 23 | func (dad *DavAccountDelete) Where(ps ...predicate.DavAccount) *DavAccountDelete { 24 | dad.mutation.Where(ps...) 25 | return dad 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (dad *DavAccountDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, dad.sqlExec, dad.mutation, dad.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (dad *DavAccountDelete) ExecX(ctx context.Context) int { 35 | n, err := dad.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (dad *DavAccountDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(davaccount.Table, sqlgraph.NewFieldSpec(davaccount.FieldID, field.TypeInt)) 44 | if ps := dad.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, dad.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | dad.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // DavAccountDeleteOne is the builder for deleting a single DavAccount entity. 60 | type DavAccountDeleteOne struct { 61 | dad *DavAccountDelete 62 | } 63 | 64 | // Where appends a list predicates to the DavAccountDelete builder. 65 | func (dado *DavAccountDeleteOne) Where(ps ...predicate.DavAccount) *DavAccountDeleteOne { 66 | dado.dad.mutation.Where(ps...) 67 | return dado 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (dado *DavAccountDeleteOne) Exec(ctx context.Context) error { 72 | n, err := dado.dad.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{davaccount.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (dado *DavAccountDeleteOne) ExecX(ctx context.Context) { 85 | if err := dado.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/directlink_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/directlink" 12 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 13 | ) 14 | 15 | // DirectLinkDelete is the builder for deleting a DirectLink entity. 16 | type DirectLinkDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *DirectLinkMutation 20 | } 21 | 22 | // Where appends a list predicates to the DirectLinkDelete builder. 23 | func (dld *DirectLinkDelete) Where(ps ...predicate.DirectLink) *DirectLinkDelete { 24 | dld.mutation.Where(ps...) 25 | return dld 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (dld *DirectLinkDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, dld.sqlExec, dld.mutation, dld.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (dld *DirectLinkDelete) ExecX(ctx context.Context) int { 35 | n, err := dld.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (dld *DirectLinkDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(directlink.Table, sqlgraph.NewFieldSpec(directlink.FieldID, field.TypeInt)) 44 | if ps := dld.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, dld.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | dld.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // DirectLinkDeleteOne is the builder for deleting a single DirectLink entity. 60 | type DirectLinkDeleteOne struct { 61 | dld *DirectLinkDelete 62 | } 63 | 64 | // Where appends a list predicates to the DirectLinkDelete builder. 65 | func (dldo *DirectLinkDeleteOne) Where(ps ...predicate.DirectLink) *DirectLinkDeleteOne { 66 | dldo.dld.mutation.Where(ps...) 67 | return dldo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (dldo *DirectLinkDeleteOne) Exec(ctx context.Context) error { 72 | n, err := dldo.dld.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{directlink.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (dldo *DirectLinkDeleteOne) ExecX(ctx context.Context) { 85 | if err := dldo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/entc.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | 8 | "entgo.io/ent/entc" 9 | "entgo.io/ent/entc/gen" 10 | ) 11 | 12 | func main() { 13 | if err := entc.Generate("./schema", &gen.Config{ 14 | Features: []gen.Feature{ 15 | gen.FeatureIntercept, 16 | gen.FeatureSnapshot, 17 | gen.FeatureUpsert, 18 | gen.FeatureUpsert, 19 | gen.FeatureExecQuery, 20 | }, 21 | Templates: []*gen.Template{ 22 | gen.MustParse(gen.NewTemplate("edge_helper").ParseFiles("templates/edgehelper.tmpl")), 23 | gen.MustParse(gen.NewTemplate("mutation_helper").ParseFiles("templates/mutationhelper.tmpl")), 24 | gen.MustParse(gen.NewTemplate("create_helper").ParseFiles("templates/createhelper.tmpl")), 25 | }, 26 | }); err != nil { 27 | log.Fatal("running ent codegen:", err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ent/entity_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/entity" 12 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 13 | ) 14 | 15 | // EntityDelete is the builder for deleting a Entity entity. 16 | type EntityDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *EntityMutation 20 | } 21 | 22 | // Where appends a list predicates to the EntityDelete builder. 23 | func (ed *EntityDelete) Where(ps ...predicate.Entity) *EntityDelete { 24 | ed.mutation.Where(ps...) 25 | return ed 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (ed *EntityDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, ed.sqlExec, ed.mutation, ed.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (ed *EntityDelete) ExecX(ctx context.Context) int { 35 | n, err := ed.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (ed *EntityDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(entity.Table, sqlgraph.NewFieldSpec(entity.FieldID, field.TypeInt)) 44 | if ps := ed.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, ed.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | ed.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // EntityDeleteOne is the builder for deleting a single Entity entity. 60 | type EntityDeleteOne struct { 61 | ed *EntityDelete 62 | } 63 | 64 | // Where appends a list predicates to the EntityDelete builder. 65 | func (edo *EntityDeleteOne) Where(ps ...predicate.Entity) *EntityDeleteOne { 66 | edo.ed.mutation.Where(ps...) 67 | return edo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (edo *EntityDeleteOne) Exec(ctx context.Context) error { 72 | n, err := edo.ed.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{entity.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (edo *EntityDeleteOne) ExecX(ctx context.Context) { 85 | if err := edo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/enttest/enttest.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package enttest 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/cloudreve/Cloudreve/v4/ent" 9 | // required by schema hooks. 10 | _ "github.com/cloudreve/Cloudreve/v4/ent/runtime" 11 | 12 | "entgo.io/ent/dialect/sql/schema" 13 | "github.com/cloudreve/Cloudreve/v4/ent/migrate" 14 | ) 15 | 16 | type ( 17 | // TestingT is the interface that is shared between 18 | // testing.T and testing.B and used by enttest. 19 | TestingT interface { 20 | FailNow() 21 | Error(...any) 22 | } 23 | 24 | // Option configures client creation. 25 | Option func(*options) 26 | 27 | options struct { 28 | opts []ent.Option 29 | migrateOpts []schema.MigrateOption 30 | } 31 | ) 32 | 33 | // WithOptions forwards options to client creation. 34 | func WithOptions(opts ...ent.Option) Option { 35 | return func(o *options) { 36 | o.opts = append(o.opts, opts...) 37 | } 38 | } 39 | 40 | // WithMigrateOptions forwards options to auto migration. 41 | func WithMigrateOptions(opts ...schema.MigrateOption) Option { 42 | return func(o *options) { 43 | o.migrateOpts = append(o.migrateOpts, opts...) 44 | } 45 | } 46 | 47 | func newOptions(opts []Option) *options { 48 | o := &options{} 49 | for _, opt := range opts { 50 | opt(o) 51 | } 52 | return o 53 | } 54 | 55 | // Open calls ent.Open and auto-run migration. 56 | func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client { 57 | o := newOptions(opts) 58 | c, err := ent.Open(driverName, dataSourceName, o.opts...) 59 | if err != nil { 60 | t.Error(err) 61 | t.FailNow() 62 | } 63 | migrateSchema(t, c, o) 64 | return c 65 | } 66 | 67 | // NewClient calls ent.NewClient and auto-run migration. 68 | func NewClient(t TestingT, opts ...Option) *ent.Client { 69 | o := newOptions(opts) 70 | c := ent.NewClient(o.opts...) 71 | migrateSchema(t, c, o) 72 | return c 73 | } 74 | func migrateSchema(t TestingT, c *ent.Client, o *options) { 75 | tables, err := schema.CopyTables(migrate.Tables) 76 | if err != nil { 77 | t.Error(err) 78 | t.FailNow() 79 | } 80 | if err := migrate.Create(context.Background(), c.Schema, tables, o.migrateOpts...); err != nil { 81 | t.Error(err) 82 | t.FailNow() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ent/file_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/file" 12 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 13 | ) 14 | 15 | // FileDelete is the builder for deleting a File entity. 16 | type FileDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *FileMutation 20 | } 21 | 22 | // Where appends a list predicates to the FileDelete builder. 23 | func (fd *FileDelete) Where(ps ...predicate.File) *FileDelete { 24 | fd.mutation.Where(ps...) 25 | return fd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (fd *FileDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, fd.sqlExec, fd.mutation, fd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (fd *FileDelete) ExecX(ctx context.Context) int { 35 | n, err := fd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (fd *FileDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(file.Table, sqlgraph.NewFieldSpec(file.FieldID, field.TypeInt)) 44 | if ps := fd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, fd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | fd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // FileDeleteOne is the builder for deleting a single File entity. 60 | type FileDeleteOne struct { 61 | fd *FileDelete 62 | } 63 | 64 | // Where appends a list predicates to the FileDelete builder. 65 | func (fdo *FileDeleteOne) Where(ps ...predicate.File) *FileDeleteOne { 66 | fdo.fd.mutation.Where(ps...) 67 | return fdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (fdo *FileDeleteOne) Exec(ctx context.Context) error { 72 | n, err := fdo.fd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{file.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (fdo *FileDeleteOne) ExecX(ctx context.Context) { 85 | if err := fdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/generate.go: -------------------------------------------------------------------------------- 1 | package ent 2 | 3 | //go:generate go run -mod=mod entc.go 4 | -------------------------------------------------------------------------------- /ent/group_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/group" 12 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 13 | ) 14 | 15 | // GroupDelete is the builder for deleting a Group entity. 16 | type GroupDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *GroupMutation 20 | } 21 | 22 | // Where appends a list predicates to the GroupDelete builder. 23 | func (gd *GroupDelete) Where(ps ...predicate.Group) *GroupDelete { 24 | gd.mutation.Where(ps...) 25 | return gd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (gd *GroupDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, gd.sqlExec, gd.mutation, gd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (gd *GroupDelete) ExecX(ctx context.Context) int { 35 | n, err := gd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (gd *GroupDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(group.Table, sqlgraph.NewFieldSpec(group.FieldID, field.TypeInt)) 44 | if ps := gd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, gd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | gd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // GroupDeleteOne is the builder for deleting a single Group entity. 60 | type GroupDeleteOne struct { 61 | gd *GroupDelete 62 | } 63 | 64 | // Where appends a list predicates to the GroupDelete builder. 65 | func (gdo *GroupDeleteOne) Where(ps ...predicate.Group) *GroupDeleteOne { 66 | gdo.gd.mutation.Where(ps...) 67 | return gdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (gdo *GroupDeleteOne) Exec(ctx context.Context) error { 72 | n, err := gdo.gd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{group.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (gdo *GroupDeleteOne) ExecX(ctx context.Context) { 85 | if err := gdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/metadata_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/metadata" 12 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 13 | ) 14 | 15 | // MetadataDelete is the builder for deleting a Metadata entity. 16 | type MetadataDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *MetadataMutation 20 | } 21 | 22 | // Where appends a list predicates to the MetadataDelete builder. 23 | func (md *MetadataDelete) Where(ps ...predicate.Metadata) *MetadataDelete { 24 | md.mutation.Where(ps...) 25 | return md 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (md *MetadataDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, md.sqlExec, md.mutation, md.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (md *MetadataDelete) ExecX(ctx context.Context) int { 35 | n, err := md.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (md *MetadataDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(metadata.Table, sqlgraph.NewFieldSpec(metadata.FieldID, field.TypeInt)) 44 | if ps := md.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, md.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | md.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // MetadataDeleteOne is the builder for deleting a single Metadata entity. 60 | type MetadataDeleteOne struct { 61 | md *MetadataDelete 62 | } 63 | 64 | // Where appends a list predicates to the MetadataDelete builder. 65 | func (mdo *MetadataDeleteOne) Where(ps ...predicate.Metadata) *MetadataDeleteOne { 66 | mdo.md.mutation.Where(ps...) 67 | return mdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (mdo *MetadataDeleteOne) Exec(ctx context.Context) error { 72 | n, err := mdo.md.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{metadata.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (mdo *MetadataDeleteOne) ExecX(ctx context.Context) { 85 | if err := mdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/migrate/migrate.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package migrate 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | 10 | "entgo.io/ent/dialect" 11 | "entgo.io/ent/dialect/sql/schema" 12 | ) 13 | 14 | var ( 15 | // WithGlobalUniqueID sets the universal ids options to the migration. 16 | // If this option is enabled, ent migration will allocate a 1<<32 range 17 | // for the ids of each entity (table). 18 | // Note that this option cannot be applied on tables that already exist. 19 | WithGlobalUniqueID = schema.WithGlobalUniqueID 20 | // WithDropColumn sets the drop column option to the migration. 21 | // If this option is enabled, ent migration will drop old columns 22 | // that were used for both fields and edges. This defaults to false. 23 | WithDropColumn = schema.WithDropColumn 24 | // WithDropIndex sets the drop index option to the migration. 25 | // If this option is enabled, ent migration will drop old indexes 26 | // that were defined in the schema. This defaults to false. 27 | // Note that unique constraints are defined using `UNIQUE INDEX`, 28 | // and therefore, it's recommended to enable this option to get more 29 | // flexibility in the schema changes. 30 | WithDropIndex = schema.WithDropIndex 31 | // WithForeignKeys enables creating foreign-key in schema DDL. This defaults to true. 32 | WithForeignKeys = schema.WithForeignKeys 33 | ) 34 | 35 | // Schema is the API for creating, migrating and dropping a schema. 36 | type Schema struct { 37 | drv dialect.Driver 38 | } 39 | 40 | // NewSchema creates a new schema client. 41 | func NewSchema(drv dialect.Driver) *Schema { return &Schema{drv: drv} } 42 | 43 | // Create creates all schema resources. 44 | func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error { 45 | return Create(ctx, s, Tables, opts...) 46 | } 47 | 48 | // Create creates all table resources using the given schema driver. 49 | func Create(ctx context.Context, s *Schema, tables []*schema.Table, opts ...schema.MigrateOption) error { 50 | migrate, err := schema.NewMigrate(s.drv, opts...) 51 | if err != nil { 52 | return fmt.Errorf("ent/migrate: %w", err) 53 | } 54 | return migrate.Create(ctx, tables...) 55 | } 56 | 57 | // WriteTo writes the schema changes to w instead of running them against the database. 58 | // 59 | // if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil { 60 | // log.Fatal(err) 61 | // } 62 | func (s *Schema) WriteTo(ctx context.Context, w io.Writer, opts ...schema.MigrateOption) error { 63 | return Create(ctx, &Schema{drv: &schema.WriteDriver{Writer: w, Driver: s.drv}}, Tables, opts...) 64 | } 65 | -------------------------------------------------------------------------------- /ent/mutationhelper.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | // SetUpdatedAt sets the "updated_at" field. 6 | 7 | func (m *DavAccountMutation) SetRawID(t int) { 8 | m.id = &t 9 | } 10 | 11 | // SetUpdatedAt sets the "updated_at" field. 12 | 13 | func (m *DirectLinkMutation) SetRawID(t int) { 14 | m.id = &t 15 | } 16 | 17 | // SetUpdatedAt sets the "updated_at" field. 18 | 19 | func (m *EntityMutation) SetRawID(t int) { 20 | m.id = &t 21 | } 22 | 23 | // SetUpdatedAt sets the "updated_at" field. 24 | 25 | func (m *FileMutation) SetRawID(t int) { 26 | m.id = &t 27 | } 28 | 29 | // SetUpdatedAt sets the "updated_at" field. 30 | 31 | func (m *GroupMutation) SetRawID(t int) { 32 | m.id = &t 33 | } 34 | 35 | // SetUpdatedAt sets the "updated_at" field. 36 | 37 | func (m *MetadataMutation) SetRawID(t int) { 38 | m.id = &t 39 | } 40 | 41 | // SetUpdatedAt sets the "updated_at" field. 42 | 43 | func (m *NodeMutation) SetRawID(t int) { 44 | m.id = &t 45 | } 46 | 47 | // SetUpdatedAt sets the "updated_at" field. 48 | 49 | func (m *PasskeyMutation) SetRawID(t int) { 50 | m.id = &t 51 | } 52 | 53 | // SetUpdatedAt sets the "updated_at" field. 54 | 55 | func (m *SettingMutation) SetRawID(t int) { 56 | m.id = &t 57 | } 58 | 59 | // SetUpdatedAt sets the "updated_at" field. 60 | 61 | func (m *ShareMutation) SetRawID(t int) { 62 | m.id = &t 63 | } 64 | 65 | // SetUpdatedAt sets the "updated_at" field. 66 | 67 | func (m *StoragePolicyMutation) SetRawID(t int) { 68 | m.id = &t 69 | } 70 | 71 | // SetUpdatedAt sets the "updated_at" field. 72 | 73 | func (m *TaskMutation) SetRawID(t int) { 74 | m.id = &t 75 | } 76 | 77 | // SetUpdatedAt sets the "updated_at" field. 78 | 79 | func (m *UserMutation) SetRawID(t int) { 80 | m.id = &t 81 | } 82 | -------------------------------------------------------------------------------- /ent/node_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/node" 12 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 13 | ) 14 | 15 | // NodeDelete is the builder for deleting a Node entity. 16 | type NodeDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *NodeMutation 20 | } 21 | 22 | // Where appends a list predicates to the NodeDelete builder. 23 | func (nd *NodeDelete) Where(ps ...predicate.Node) *NodeDelete { 24 | nd.mutation.Where(ps...) 25 | return nd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (nd *NodeDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, nd.sqlExec, nd.mutation, nd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (nd *NodeDelete) ExecX(ctx context.Context) int { 35 | n, err := nd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (nd *NodeDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(node.Table, sqlgraph.NewFieldSpec(node.FieldID, field.TypeInt)) 44 | if ps := nd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, nd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | nd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // NodeDeleteOne is the builder for deleting a single Node entity. 60 | type NodeDeleteOne struct { 61 | nd *NodeDelete 62 | } 63 | 64 | // Where appends a list predicates to the NodeDelete builder. 65 | func (ndo *NodeDeleteOne) Where(ps ...predicate.Node) *NodeDeleteOne { 66 | ndo.nd.mutation.Where(ps...) 67 | return ndo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (ndo *NodeDeleteOne) Exec(ctx context.Context) error { 72 | n, err := ndo.nd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{node.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (ndo *NodeDeleteOne) ExecX(ctx context.Context) { 85 | if err := ndo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/passkey_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/passkey" 12 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 13 | ) 14 | 15 | // PasskeyDelete is the builder for deleting a Passkey entity. 16 | type PasskeyDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *PasskeyMutation 20 | } 21 | 22 | // Where appends a list predicates to the PasskeyDelete builder. 23 | func (pd *PasskeyDelete) Where(ps ...predicate.Passkey) *PasskeyDelete { 24 | pd.mutation.Where(ps...) 25 | return pd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (pd *PasskeyDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, pd.sqlExec, pd.mutation, pd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (pd *PasskeyDelete) ExecX(ctx context.Context) int { 35 | n, err := pd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (pd *PasskeyDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(passkey.Table, sqlgraph.NewFieldSpec(passkey.FieldID, field.TypeInt)) 44 | if ps := pd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, pd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | pd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // PasskeyDeleteOne is the builder for deleting a single Passkey entity. 60 | type PasskeyDeleteOne struct { 61 | pd *PasskeyDelete 62 | } 63 | 64 | // Where appends a list predicates to the PasskeyDelete builder. 65 | func (pdo *PasskeyDeleteOne) Where(ps ...predicate.Passkey) *PasskeyDeleteOne { 66 | pdo.pd.mutation.Where(ps...) 67 | return pdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (pdo *PasskeyDeleteOne) Exec(ctx context.Context) error { 72 | n, err := pdo.pd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{passkey.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (pdo *PasskeyDeleteOne) ExecX(ctx context.Context) { 85 | if err := pdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/predicate/predicate.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package predicate 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | // DavAccount is the predicate function for davaccount builders. 10 | type DavAccount func(*sql.Selector) 11 | 12 | // DirectLink is the predicate function for directlink builders. 13 | type DirectLink func(*sql.Selector) 14 | 15 | // Entity is the predicate function for entity builders. 16 | type Entity func(*sql.Selector) 17 | 18 | // File is the predicate function for file builders. 19 | type File func(*sql.Selector) 20 | 21 | // Group is the predicate function for group builders. 22 | type Group func(*sql.Selector) 23 | 24 | // Metadata is the predicate function for metadata builders. 25 | type Metadata func(*sql.Selector) 26 | 27 | // Node is the predicate function for node builders. 28 | type Node func(*sql.Selector) 29 | 30 | // Passkey is the predicate function for passkey builders. 31 | type Passkey func(*sql.Selector) 32 | 33 | // Setting is the predicate function for setting builders. 34 | type Setting func(*sql.Selector) 35 | 36 | // Share is the predicate function for share builders. 37 | type Share func(*sql.Selector) 38 | 39 | // StoragePolicy is the predicate function for storagepolicy builders. 40 | type StoragePolicy func(*sql.Selector) 41 | 42 | // Task is the predicate function for task builders. 43 | type Task func(*sql.Selector) 44 | 45 | // User is the predicate function for user builders. 46 | type User func(*sql.Selector) 47 | -------------------------------------------------------------------------------- /ent/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | // The schema-stitching logic is generated in github.com/cloudreve/Cloudreve/v4/ent/runtime/runtime.go 6 | -------------------------------------------------------------------------------- /ent/schema/davaccount.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "entgo.io/ent/schema/index" 8 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 9 | "github.com/cloudreve/Cloudreve/v4/pkg/boolset" 10 | ) 11 | 12 | // DavAccount holds the schema definition for the DavAccount entity. 13 | type DavAccount struct { 14 | ent.Schema 15 | } 16 | 17 | // Fields of the DavAccount. 18 | func (DavAccount) Fields() []ent.Field { 19 | return []ent.Field{ 20 | field.String("name"), 21 | field.Text("uri"), 22 | field.String("password"). 23 | Sensitive(), 24 | field.Bytes("options").GoType(&boolset.BooleanSet{}), 25 | field.JSON("props", &types.DavAccountProps{}). 26 | Optional(), 27 | field.Int("owner_id"), 28 | } 29 | } 30 | 31 | // Edges of the DavAccount. 32 | func (DavAccount) Edges() []ent.Edge { 33 | return []ent.Edge{ 34 | edge.From("owner", User.Type). 35 | Ref("dav_accounts"). 36 | Field("owner_id"). 37 | Unique(). 38 | Required(), 39 | } 40 | } 41 | 42 | // Indexes of the DavAccount. 43 | func (DavAccount) Indexes() []ent.Index { 44 | return []ent.Index{ 45 | index.Fields("owner_id", "password"). 46 | Unique(), 47 | } 48 | } 49 | 50 | func (DavAccount) Mixin() []ent.Mixin { 51 | return []ent.Mixin{ 52 | CommonMixin{}, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ent/schema/directlink.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | ) 8 | 9 | // DirectLink holds the schema definition for the DirectLink entity. 10 | type DirectLink struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the DirectLink. 15 | func (DirectLink) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("name"), 18 | field.Int("downloads"), 19 | field.Int("file_id"), 20 | field.Int("speed"), 21 | } 22 | } 23 | 24 | // Edges of the DirectLink. 25 | func (DirectLink) Edges() []ent.Edge { 26 | return []ent.Edge{ 27 | edge.From("file", File.Type). 28 | Ref("direct_links"). 29 | Field("file_id"). 30 | Required(). 31 | Unique(), 32 | } 33 | } 34 | 35 | func (DirectLink) Mixin() []ent.Mixin { 36 | return []ent.Mixin{ 37 | CommonMixin{}, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ent/schema/entity.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 8 | "github.com/gofrs/uuid" 9 | ) 10 | 11 | // Entity holds the schema definition for the Entity. 12 | type Entity struct { 13 | ent.Schema 14 | } 15 | 16 | // Fields of the Entity. 17 | func (Entity) Fields() []ent.Field { 18 | return []ent.Field{ 19 | field.Int("type"), 20 | field.Text("source"), 21 | field.Int64("size"), 22 | field.Int("reference_count").Default(1), 23 | field.Int("storage_policy_entities"), 24 | field.Int("created_by").Optional(), 25 | field.UUID("upload_session_id", uuid.Must(uuid.NewV4())). 26 | Optional(). 27 | Nillable(), 28 | field.JSON("recycle_options", &types.EntityRecycleOption{}). 29 | Optional(), 30 | } 31 | } 32 | 33 | // Edges of the Entity. 34 | func (Entity) Edges() []ent.Edge { 35 | return []ent.Edge{ 36 | edge.From("file", File.Type). 37 | Ref("entities"), 38 | edge.From("user", User.Type). 39 | Field("created_by"). 40 | Unique(). 41 | Ref("entities"), 42 | edge.From("storage_policy", StoragePolicy.Type). 43 | Ref("entities"). 44 | Field("storage_policy_entities"). 45 | Unique(). 46 | Required(), 47 | } 48 | } 49 | 50 | func (Entity) Mixin() []ent.Mixin { 51 | return []ent.Mixin{ 52 | CommonMixin{}, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ent/schema/file.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "entgo.io/ent/schema/index" 8 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 9 | ) 10 | 11 | // File holds the schema definition for the File entity. 12 | type File struct { 13 | ent.Schema 14 | } 15 | 16 | // Fields of the File. 17 | func (File) Fields() []ent.Field { 18 | return []ent.Field{ 19 | field.Int("type"), 20 | field.String("name"), 21 | field.Int("owner_id"), 22 | field.Int64("size"). 23 | Default(0), 24 | field.Int("primary_entity"). 25 | Optional(), 26 | field.Int("file_children"). 27 | Optional(), 28 | field.Bool("is_symbolic"). 29 | Default(false), 30 | field.JSON("props", &types.FileProps{}).Optional(), 31 | field.Int("storage_policy_files"). 32 | Optional(), 33 | } 34 | } 35 | 36 | // Edges of the File. 37 | func (File) Edges() []ent.Edge { 38 | return []ent.Edge{ 39 | edge.From("owner", User.Type). 40 | Ref("files"). 41 | Field("owner_id"). 42 | Unique(). 43 | Required(), 44 | edge.From("storage_policies", StoragePolicy.Type). 45 | Ref("files"). 46 | Field("storage_policy_files"). 47 | Unique(), 48 | edge.To("children", File.Type). 49 | From("parent"). 50 | Field("file_children"). 51 | Unique(), 52 | edge.To("metadata", Metadata.Type), 53 | edge.To("entities", Entity.Type), 54 | edge.To("shares", Share.Type), 55 | edge.To("direct_links", DirectLink.Type), 56 | } 57 | } 58 | 59 | // Indexes of the File. 60 | func (File) Indexes() []ent.Index { 61 | return []ent.Index{ 62 | index.Fields("file_children", "name"). 63 | Unique(), 64 | index.Fields("file_children", "type", "updated_at"), 65 | index.Fields("file_children", "type", "size"), 66 | } 67 | } 68 | 69 | func (File) Mixin() []ent.Mixin { 70 | return []ent.Mixin{ 71 | CommonMixin{}, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ent/schema/group.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/boolset" 9 | ) 10 | 11 | // Group holds the schema definition for the Group entity. 12 | type Group struct { 13 | ent.Schema 14 | } 15 | 16 | func (Group) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("name"), 19 | field.Int64("max_storage"). 20 | Optional(), 21 | field.Int("speed_limit"). 22 | Optional(), 23 | field.Bytes("permissions").GoType(&boolset.BooleanSet{}), 24 | field.JSON("settings", &types.GroupSetting{}). 25 | Default(&types.GroupSetting{}). 26 | Optional(), 27 | field.Int("storage_policy_id").Optional(), 28 | } 29 | } 30 | 31 | func (Group) Mixin() []ent.Mixin { 32 | return []ent.Mixin{ 33 | CommonMixin{}, 34 | } 35 | } 36 | 37 | func (Group) Edges() []ent.Edge { 38 | return []ent.Edge{ 39 | edge.To("users", User.Type), 40 | edge.From("storage_policies", StoragePolicy.Type). 41 | Ref("groups"). 42 | Field("storage_policy_id"). 43 | Unique(), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ent/schema/metadata.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "entgo.io/ent/schema/index" 8 | ) 9 | 10 | // Metadata holds the schema definition for the Metadata entity. 11 | type Metadata struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the Metadata. 16 | func (Metadata) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("name"), 19 | field.Text("value"), 20 | field.Int("file_id"), 21 | field.Bool("is_public"). 22 | Default(false), 23 | } 24 | } 25 | 26 | // Edges of the Metadata. 27 | func (Metadata) Edges() []ent.Edge { 28 | return []ent.Edge{ 29 | edge.From("file", File.Type). 30 | Ref("metadata"). 31 | Field("file_id"). 32 | Required(). 33 | Unique(), 34 | } 35 | } 36 | 37 | func (Metadata) Indexes() []ent.Index { 38 | return []ent.Index{ 39 | index.Fields("file_id", "name"). 40 | Unique(), 41 | } 42 | } 43 | 44 | func (Metadata) Mixin() []ent.Mixin { 45 | return []ent.Mixin{ 46 | CommonMixin{}, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ent/schema/node.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/boolset" 9 | ) 10 | 11 | // Node holds the schema definition for the Node entity. 12 | type Node struct { 13 | ent.Schema 14 | } 15 | 16 | // Fields of the Node. 17 | func (Node) Fields() []ent.Field { 18 | return []ent.Field{ 19 | field.Enum("status"). 20 | Values("active", "suspended"), 21 | field.String("name"), 22 | field.Enum("type"). 23 | Values("master", "slave"), 24 | field.String("server"). 25 | Optional(), 26 | field.String("slave_key").Optional(), 27 | field.Bytes("capabilities").GoType(&boolset.BooleanSet{}), 28 | field.JSON("settings", &types.NodeSetting{}). 29 | Default(&types.NodeSetting{}). 30 | Optional(), 31 | field.Int("weight").Default(0), 32 | } 33 | } 34 | 35 | // Edges of the Node. 36 | func (Node) Edges() []ent.Edge { 37 | return []ent.Edge{ 38 | edge.To("storage_policy", StoragePolicy.Type), 39 | } 40 | } 41 | 42 | func (Node) Mixin() []ent.Mixin { 43 | return []ent.Mixin{ 44 | CommonMixin{}, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ent/schema/passkey.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/dialect" 6 | "entgo.io/ent/schema/edge" 7 | "entgo.io/ent/schema/field" 8 | "entgo.io/ent/schema/index" 9 | "github.com/go-webauthn/webauthn/webauthn" 10 | ) 11 | 12 | // Passkey holds the schema definition for the Passkey entity. 13 | type Passkey struct { 14 | ent.Schema 15 | } 16 | 17 | // Fields of the Passkey. 18 | func (Passkey) Fields() []ent.Field { 19 | return []ent.Field{ 20 | field.Int("user_id"), 21 | field.String("credential_id"), 22 | field.String("name"), 23 | field.JSON("credential", &webauthn.Credential{}). 24 | Sensitive(), 25 | field.Time("used_at"). 26 | Optional(). 27 | Nillable(). 28 | SchemaType(map[string]string{ 29 | dialect.MySQL: "datetime", 30 | }), 31 | } 32 | } 33 | 34 | // Edges of the Passkey. 35 | func (Passkey) Edges() []ent.Edge { 36 | return []ent.Edge{ 37 | edge.From("user", User.Type). 38 | Field("user_id"). 39 | Ref("passkey"). 40 | Unique(). 41 | Required(), 42 | } 43 | } 44 | 45 | func (Passkey) Mixin() []ent.Mixin { 46 | return []ent.Mixin{ 47 | CommonMixin{}, 48 | } 49 | } 50 | 51 | func (Passkey) Indexes() []ent.Index { 52 | return []ent.Index{ 53 | index.Fields("user_id", "credential_id").Unique(), 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ent/schema/policy.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 8 | ) 9 | 10 | // StoragePolicy holds the schema definition for the storage policy entity. 11 | type StoragePolicy struct { 12 | ent.Schema 13 | } 14 | 15 | func (StoragePolicy) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("name"), 18 | field.String("type"), 19 | field.String("server"). 20 | Optional(), 21 | field.String("bucket_name"). 22 | Optional(), 23 | field.Bool("is_private"). 24 | Optional(), 25 | field.Text("access_key"). 26 | Optional(), 27 | field.Text("secret_key"). 28 | Optional(), 29 | field.Int64("max_size"). 30 | Optional(), 31 | field.String("dir_name_rule"). 32 | Optional(), 33 | field.String("file_name_rule"). 34 | Optional(), 35 | field.JSON("settings", &types.PolicySetting{}). 36 | Default(&types.PolicySetting{}). 37 | Optional(), 38 | field.Int("node_id").Optional(), 39 | } 40 | } 41 | 42 | func (StoragePolicy) Mixin() []ent.Mixin { 43 | return []ent.Mixin{ 44 | CommonMixin{}, 45 | } 46 | } 47 | 48 | func (StoragePolicy) Edges() []ent.Edge { 49 | return []ent.Edge{ 50 | edge.To("groups", Group.Type), 51 | edge.To("files", File.Type), 52 | edge.To("entities", Entity.Type), 53 | edge.From("node", Node.Type). 54 | Ref("storage_policy"). 55 | Field("node_id"). 56 | Unique(), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ent/schema/setting.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/field" 6 | ) 7 | 8 | // Setting holds the schema definition for key-value setting entity. 9 | type Setting struct { 10 | ent.Schema 11 | } 12 | 13 | func (Setting) Fields() []ent.Field { 14 | return []ent.Field{ 15 | field.String("name"). 16 | Unique(), 17 | field.Text("value"). 18 | Optional(), 19 | } 20 | } 21 | 22 | func (Setting) Edges() []ent.Edge { 23 | return nil 24 | } 25 | 26 | func (Setting) Mixin() []ent.Mixin { 27 | return []ent.Mixin{ 28 | CommonMixin{}, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ent/schema/share.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/dialect" 6 | "entgo.io/ent/schema/edge" 7 | "entgo.io/ent/schema/field" 8 | ) 9 | 10 | // Share holds the schema definition for the Share entity. 11 | type Share struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the Share. 16 | func (Share) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("password"). 19 | Optional(), 20 | field.Int("views"). 21 | Default(0), 22 | field.Int("downloads"). 23 | Default(0), 24 | field.Time("expires"). 25 | Nillable(). 26 | Optional(). 27 | SchemaType(map[string]string{ 28 | dialect.MySQL: "datetime", 29 | }), 30 | field.Int("remain_downloads"). 31 | Nillable(). 32 | Optional(), 33 | } 34 | } 35 | 36 | // Edges of the Share. 37 | func (Share) Edges() []ent.Edge { 38 | return []ent.Edge{ 39 | edge.From("user", User.Type). 40 | Ref("shares").Unique(), 41 | edge.From("file", File.Type). 42 | Ref("shares").Unique(), 43 | } 44 | } 45 | 46 | func (Share) Mixin() []ent.Mixin { 47 | return []ent.Mixin{ 48 | CommonMixin{}, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ent/schema/task.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 8 | "github.com/gofrs/uuid" 9 | ) 10 | 11 | // Task holds the schema definition for the Task entity. 12 | type Task struct { 13 | ent.Schema 14 | } 15 | 16 | // Fields of the Task. 17 | func (Task) Fields() []ent.Field { 18 | return []ent.Field{ 19 | field.String("type"), 20 | field.Enum("status"). 21 | Values("queued", "processing", "suspending", "error", "canceled", "completed"). 22 | Default("queued"), 23 | field.JSON("public_state", &types.TaskPublicState{}), 24 | field.Text("private_state").Optional(), 25 | field.UUID("correlation_id", uuid.Must(uuid.NewV4())). 26 | Optional(). 27 | Immutable(), 28 | field.Int("user_tasks").Optional(), 29 | } 30 | } 31 | 32 | // Edges of the Task. 33 | func (Task) Edges() []ent.Edge { 34 | return []ent.Edge{ 35 | edge.From("user", User.Type). 36 | Ref("tasks"). 37 | Field("user_tasks"). 38 | Unique(), 39 | } 40 | } 41 | 42 | func (Task) Mixin() []ent.Mixin { 43 | return []ent.Mixin{ 44 | CommonMixin{}, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ent/schema/user.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 8 | ) 9 | 10 | // User holds the schema definition for the User entity. 11 | type User struct { 12 | ent.Schema 13 | } 14 | 15 | func (User) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("email"). 18 | MaxLen(100). 19 | Unique(), 20 | field.String("nick"). 21 | MaxLen(100), 22 | field.String("password"). 23 | Optional(). 24 | Sensitive(), 25 | field.Enum("status"). 26 | Values("active", "inactive", "manual_banned", "sys_banned"). 27 | Default("active"), 28 | field.Int64("storage"). 29 | Default(0), 30 | field.String("two_factor_secret"). 31 | Sensitive(). 32 | Optional(), 33 | field.String("avatar"). 34 | Optional(), 35 | field.JSON("settings", &types.UserSetting{}). 36 | Default(&types.UserSetting{}). 37 | Optional(), 38 | field.Int("group_users"), 39 | } 40 | } 41 | 42 | func (User) Edges() []ent.Edge { 43 | return []ent.Edge{ 44 | edge.From("group", Group.Type). 45 | Ref("users"). 46 | Field("group_users"). 47 | Unique(). 48 | Required(), 49 | edge.To("files", File.Type), 50 | edge.To("dav_accounts", DavAccount.Type), 51 | edge.To("shares", Share.Type), 52 | edge.To("passkey", Passkey.Type), 53 | edge.To("tasks", Task.Type), 54 | edge.To("entities", Entity.Type), 55 | } 56 | } 57 | 58 | func (User) Mixin() []ent.Mixin { 59 | return []ent.Mixin{ 60 | CommonMixin{}, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ent/setting_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 12 | "github.com/cloudreve/Cloudreve/v4/ent/setting" 13 | ) 14 | 15 | // SettingDelete is the builder for deleting a Setting entity. 16 | type SettingDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *SettingMutation 20 | } 21 | 22 | // Where appends a list predicates to the SettingDelete builder. 23 | func (sd *SettingDelete) Where(ps ...predicate.Setting) *SettingDelete { 24 | sd.mutation.Where(ps...) 25 | return sd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (sd *SettingDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, sd.sqlExec, sd.mutation, sd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (sd *SettingDelete) ExecX(ctx context.Context) int { 35 | n, err := sd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (sd *SettingDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(setting.Table, sqlgraph.NewFieldSpec(setting.FieldID, field.TypeInt)) 44 | if ps := sd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, sd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | sd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // SettingDeleteOne is the builder for deleting a single Setting entity. 60 | type SettingDeleteOne struct { 61 | sd *SettingDelete 62 | } 63 | 64 | // Where appends a list predicates to the SettingDelete builder. 65 | func (sdo *SettingDeleteOne) Where(ps ...predicate.Setting) *SettingDeleteOne { 66 | sdo.sd.mutation.Where(ps...) 67 | return sdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (sdo *SettingDeleteOne) Exec(ctx context.Context) error { 72 | n, err := sdo.sd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{setting.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (sdo *SettingDeleteOne) ExecX(ctx context.Context) { 85 | if err := sdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/share_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 12 | "github.com/cloudreve/Cloudreve/v4/ent/share" 13 | ) 14 | 15 | // ShareDelete is the builder for deleting a Share entity. 16 | type ShareDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *ShareMutation 20 | } 21 | 22 | // Where appends a list predicates to the ShareDelete builder. 23 | func (sd *ShareDelete) Where(ps ...predicate.Share) *ShareDelete { 24 | sd.mutation.Where(ps...) 25 | return sd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (sd *ShareDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, sd.sqlExec, sd.mutation, sd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (sd *ShareDelete) ExecX(ctx context.Context) int { 35 | n, err := sd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (sd *ShareDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(share.Table, sqlgraph.NewFieldSpec(share.FieldID, field.TypeInt)) 44 | if ps := sd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, sd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | sd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // ShareDeleteOne is the builder for deleting a single Share entity. 60 | type ShareDeleteOne struct { 61 | sd *ShareDelete 62 | } 63 | 64 | // Where appends a list predicates to the ShareDelete builder. 65 | func (sdo *ShareDeleteOne) Where(ps ...predicate.Share) *ShareDeleteOne { 66 | sdo.sd.mutation.Where(ps...) 67 | return sdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (sdo *ShareDeleteOne) Exec(ctx context.Context) error { 72 | n, err := sdo.sd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{share.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (sdo *ShareDeleteOne) ExecX(ctx context.Context) { 85 | if err := sdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/storagepolicy_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 12 | "github.com/cloudreve/Cloudreve/v4/ent/storagepolicy" 13 | ) 14 | 15 | // StoragePolicyDelete is the builder for deleting a StoragePolicy entity. 16 | type StoragePolicyDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *StoragePolicyMutation 20 | } 21 | 22 | // Where appends a list predicates to the StoragePolicyDelete builder. 23 | func (spd *StoragePolicyDelete) Where(ps ...predicate.StoragePolicy) *StoragePolicyDelete { 24 | spd.mutation.Where(ps...) 25 | return spd 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (spd *StoragePolicyDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, spd.sqlExec, spd.mutation, spd.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (spd *StoragePolicyDelete) ExecX(ctx context.Context) int { 35 | n, err := spd.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (spd *StoragePolicyDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(storagepolicy.Table, sqlgraph.NewFieldSpec(storagepolicy.FieldID, field.TypeInt)) 44 | if ps := spd.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, spd.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | spd.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // StoragePolicyDeleteOne is the builder for deleting a single StoragePolicy entity. 60 | type StoragePolicyDeleteOne struct { 61 | spd *StoragePolicyDelete 62 | } 63 | 64 | // Where appends a list predicates to the StoragePolicyDelete builder. 65 | func (spdo *StoragePolicyDeleteOne) Where(ps ...predicate.StoragePolicy) *StoragePolicyDeleteOne { 66 | spdo.spd.mutation.Where(ps...) 67 | return spdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (spdo *StoragePolicyDeleteOne) Exec(ctx context.Context) error { 72 | n, err := spdo.spd.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{storagepolicy.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (spdo *StoragePolicyDeleteOne) ExecX(ctx context.Context) { 85 | if err := spdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/task_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 12 | "github.com/cloudreve/Cloudreve/v4/ent/task" 13 | ) 14 | 15 | // TaskDelete is the builder for deleting a Task entity. 16 | type TaskDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *TaskMutation 20 | } 21 | 22 | // Where appends a list predicates to the TaskDelete builder. 23 | func (td *TaskDelete) Where(ps ...predicate.Task) *TaskDelete { 24 | td.mutation.Where(ps...) 25 | return td 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (td *TaskDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, td.sqlExec, td.mutation, td.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (td *TaskDelete) ExecX(ctx context.Context) int { 35 | n, err := td.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (td *TaskDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(task.Table, sqlgraph.NewFieldSpec(task.FieldID, field.TypeInt)) 44 | if ps := td.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, td.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | td.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // TaskDeleteOne is the builder for deleting a single Task entity. 60 | type TaskDeleteOne struct { 61 | td *TaskDelete 62 | } 63 | 64 | // Where appends a list predicates to the TaskDelete builder. 65 | func (tdo *TaskDeleteOne) Where(ps ...predicate.Task) *TaskDeleteOne { 66 | tdo.td.mutation.Where(ps...) 67 | return tdo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (tdo *TaskDeleteOne) Exec(ctx context.Context) error { 72 | n, err := tdo.td.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{task.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (tdo *TaskDeleteOne) ExecX(ctx context.Context) { 85 | if err := tdo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ent/templates/createhelper.tmpl: -------------------------------------------------------------------------------- 1 | {{/* The line below tells Intellij/GoLand to enable the autocompletion based *gen.Type type. */}} 2 | {{/* gotype: entgo.io/ent/entc/gen.Type */}} 3 | 4 | {{ define "create/additional/createhelper" }} 5 | 6 | {{/* A template that adds the "GoString" method to all generated models on the same file they are defined. */}} 7 | 8 | {{ if $.HasOneFieldID }} 9 | 10 | func (m *{{ $.Name }}Create) SetRawID(t {{ $.ID.Type }}) *{{ $.Name }}Create { 11 | m.mutation.SetRawID(t) 12 | return m 13 | } 14 | 15 | {{ end }} 16 | 17 | {{ end }} 18 | 19 | {{ define "dialect/sql/create/spec/createhelper" }} 20 | {{ $receiver := $.Scope.Receiver }} 21 | {{ $mutation := print $receiver ".mutation" }} 22 | {{- if not $.HasCompositeID}} 23 | if id, ok := {{ $mutation }}.{{ $.ID.MutationGet }}(); ok { 24 | _node.ID = id 25 | id64 := int64(id) 26 | _spec.ID.Value = {{ if and $.ID.Type.ValueScanner (not $.ID.Type.RType.IsPtr) }}&{{ end }}id64 27 | } 28 | {{- end }} 29 | 30 | {{ end }} -------------------------------------------------------------------------------- /ent/templates/edgehelper.tmpl: -------------------------------------------------------------------------------- 1 | {{/* The line below tells Intellij/GoLand to enable the autocompletion based *gen.Type type. */}} 2 | {{/* gotype: entgo.io/ent/entc/gen.Type */}} 3 | 4 | {{ define "model/additional/edgehelper" }} 5 | 6 | {{/* A template that adds the "GoString" method to all generated models on the same file they are defined. */}} 7 | 8 | {{- with $.Edges }} 9 | 10 | {{- range $i, $e := . }} 11 | // Set{{ $e.StructField }} manually set the edge as loaded state. 12 | func (e *{{ $.Name }}) Set{{ $e.StructField }}(v {{ if not $e.Unique }}[]{{ end }}*{{ $e.Type.Name }}) { 13 | e.Edges.{{ $e.StructField }} = v 14 | e.Edges.loadedTypes[{{ $i }}] = true 15 | } 16 | {{- end }} 17 | {{- end }} 18 | 19 | 20 | {{ end }} -------------------------------------------------------------------------------- /ent/templates/mutationhelper.tmpl: -------------------------------------------------------------------------------- 1 | {{/* The line below tells Intellij/GoLand to enable the autocompletion based on the *gen.Graph type. */}} 2 | {{/* gotype: entgo.io/ent/entc/gen.Graph */}} 3 | 4 | {{ define "mutationhelper" }} 5 | 6 | {{ $pkg := base $.Config.Package }} 7 | {{ template "header" $ }} 8 | 9 | {{ range $n := $.Nodes }} 10 | // SetUpdatedAt sets the "updated_at" field. 11 | {{ with $n.HasOneFieldID }} 12 | 13 | func (m *{{ $n.Name }}Mutation) SetRawID(t {{ $n.ID.Type }}) { 14 | m.id = &t 15 | } 16 | 17 | {{ end }} 18 | {{ end }} 19 | 20 | {{ end }} -------------------------------------------------------------------------------- /ent/user_delete.go: -------------------------------------------------------------------------------- 1 | // Code generated by ent, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | 8 | "entgo.io/ent/dialect/sql" 9 | "entgo.io/ent/dialect/sql/sqlgraph" 10 | "entgo.io/ent/schema/field" 11 | "github.com/cloudreve/Cloudreve/v4/ent/predicate" 12 | "github.com/cloudreve/Cloudreve/v4/ent/user" 13 | ) 14 | 15 | // UserDelete is the builder for deleting a User entity. 16 | type UserDelete struct { 17 | config 18 | hooks []Hook 19 | mutation *UserMutation 20 | } 21 | 22 | // Where appends a list predicates to the UserDelete builder. 23 | func (ud *UserDelete) Where(ps ...predicate.User) *UserDelete { 24 | ud.mutation.Where(ps...) 25 | return ud 26 | } 27 | 28 | // Exec executes the deletion query and returns how many vertices were deleted. 29 | func (ud *UserDelete) Exec(ctx context.Context) (int, error) { 30 | return withHooks(ctx, ud.sqlExec, ud.mutation, ud.hooks) 31 | } 32 | 33 | // ExecX is like Exec, but panics if an error occurs. 34 | func (ud *UserDelete) ExecX(ctx context.Context) int { 35 | n, err := ud.Exec(ctx) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return n 40 | } 41 | 42 | func (ud *UserDelete) sqlExec(ctx context.Context) (int, error) { 43 | _spec := sqlgraph.NewDeleteSpec(user.Table, sqlgraph.NewFieldSpec(user.FieldID, field.TypeInt)) 44 | if ps := ud.mutation.predicates; len(ps) > 0 { 45 | _spec.Predicate = func(selector *sql.Selector) { 46 | for i := range ps { 47 | ps[i](selector) 48 | } 49 | } 50 | } 51 | affected, err := sqlgraph.DeleteNodes(ctx, ud.driver, _spec) 52 | if err != nil && sqlgraph.IsConstraintError(err) { 53 | err = &ConstraintError{msg: err.Error(), wrap: err} 54 | } 55 | ud.mutation.done = true 56 | return affected, err 57 | } 58 | 59 | // UserDeleteOne is the builder for deleting a single User entity. 60 | type UserDeleteOne struct { 61 | ud *UserDelete 62 | } 63 | 64 | // Where appends a list predicates to the UserDelete builder. 65 | func (udo *UserDeleteOne) Where(ps ...predicate.User) *UserDeleteOne { 66 | udo.ud.mutation.Where(ps...) 67 | return udo 68 | } 69 | 70 | // Exec executes the deletion query. 71 | func (udo *UserDeleteOne) Exec(ctx context.Context) error { 72 | n, err := udo.ud.Exec(ctx) 73 | switch { 74 | case err != nil: 75 | return err 76 | case n == 0: 77 | return &NotFoundError{user.Label} 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | // ExecX is like Exec, but panics if an error occurs. 84 | func (udo *UserDeleteOne) ExecX(ctx context.Context) { 85 | if err := udo.Exec(ctx); err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /giscus.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultCommentOrder": "newest" 3 | } 4 | -------------------------------------------------------------------------------- /inventory/direct_link.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cloudreve/Cloudreve/v4/ent" 7 | "github.com/cloudreve/Cloudreve/v4/ent/directlink" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/conf" 9 | "github.com/cloudreve/Cloudreve/v4/pkg/hashid" 10 | ) 11 | 12 | type ( 13 | DirectLinkClient interface { 14 | TxOperator 15 | // GetByNameID get direct link by name and id 16 | GetByNameID(ctx context.Context, id int, name string) (*ent.DirectLink, error) 17 | // GetByID get direct link by id 18 | GetByID(ctx context.Context, id int) (*ent.DirectLink, error) 19 | } 20 | LoadDirectLinkFile struct{} 21 | ) 22 | 23 | func NewDirectLinkClient(client *ent.Client, dbType conf.DBType, hasher hashid.Encoder) DirectLinkClient { 24 | return &directLinkClient{ 25 | client: client, 26 | hasher: hasher, 27 | maxSQlParam: sqlParamLimit(dbType), 28 | } 29 | } 30 | 31 | type directLinkClient struct { 32 | maxSQlParam int 33 | client *ent.Client 34 | hasher hashid.Encoder 35 | } 36 | 37 | func (c *directLinkClient) SetClient(newClient *ent.Client) TxOperator { 38 | return &directLinkClient{client: newClient, hasher: c.hasher, maxSQlParam: c.maxSQlParam} 39 | } 40 | 41 | func (c *directLinkClient) GetClient() *ent.Client { 42 | return c.client 43 | } 44 | 45 | func (d *directLinkClient) GetByID(ctx context.Context, id int) (*ent.DirectLink, error) { 46 | return withDirectLinkEagerLoading(ctx, d.client.DirectLink.Query().Where(directlink.ID(id))). 47 | First(ctx) 48 | } 49 | 50 | func (d *directLinkClient) GetByNameID(ctx context.Context, id int, name string) (*ent.DirectLink, error) { 51 | res, err := withDirectLinkEagerLoading(ctx, d.client.DirectLink.Query().Where(directlink.ID(id), directlink.Name(name))). 52 | First(ctx) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // Increase download counter 58 | _, _ = d.client.DirectLink.Update().Where(directlink.ID(res.ID)).SetDownloads(res.Downloads + 1).Save(ctx) 59 | 60 | return res, nil 61 | } 62 | 63 | func withDirectLinkEagerLoading(ctx context.Context, q *ent.DirectLinkQuery) *ent.DirectLinkQuery { 64 | if v, ok := ctx.Value(LoadDirectLinkFile{}).(bool); ok && v { 65 | q.WithFile(func(m *ent.FileQuery) { 66 | withFileEagerLoading(ctx, m) 67 | }) 68 | } 69 | return q 70 | } 71 | -------------------------------------------------------------------------------- /inventory/tx.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloudreve/Cloudreve/v4/ent" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/logging" 8 | ) 9 | 10 | type TxOperator interface { 11 | SetClient(newClient *ent.Client) TxOperator 12 | GetClient() *ent.Client 13 | } 14 | 15 | type ( 16 | Tx struct { 17 | tx *ent.Tx 18 | parent *Tx 19 | inherited bool 20 | finished bool 21 | storageDiff StorageDiff 22 | } 23 | 24 | // TxCtx is the context key for inherited transaction 25 | TxCtx struct{} 26 | ) 27 | 28 | // AppendStorageDiff appends the given storage diff to the transaction. 29 | func (t *Tx) AppendStorageDiff(diff StorageDiff) { 30 | root := t 31 | for root.inherited { 32 | root = root.parent 33 | } 34 | 35 | if root.storageDiff == nil { 36 | root.storageDiff = diff 37 | } else { 38 | root.storageDiff.Merge(diff) 39 | } 40 | } 41 | 42 | // WithTx wraps the given inventory client with a transaction. 43 | func WithTx[T TxOperator](ctx context.Context, c T) (T, *Tx, context.Context, error) { 44 | var txClient *ent.Client 45 | var txWrapper *Tx 46 | 47 | if txInherited, ok := ctx.Value(TxCtx{}).(*Tx); ok && !txInherited.finished { 48 | txWrapper = &Tx{inherited: true, tx: txInherited.tx, parent: txInherited} 49 | } else { 50 | tx, err := c.GetClient().Tx(ctx) 51 | if err != nil { 52 | return c, nil, ctx, fmt.Errorf("failed to create transaction: %w", err) 53 | } 54 | 55 | txWrapper = &Tx{inherited: false, tx: tx} 56 | ctx = context.WithValue(ctx, TxCtx{}, txWrapper) 57 | } 58 | 59 | txClient = txWrapper.tx.Client() 60 | return c.SetClient(txClient).(T), txWrapper, ctx, nil 61 | } 62 | 63 | func Rollback(tx *Tx) error { 64 | if !tx.inherited { 65 | tx.finished = true 66 | return tx.tx.Rollback() 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func commit(tx *Tx) (bool, error) { 73 | if !tx.inherited { 74 | tx.finished = true 75 | return true, tx.tx.Commit() 76 | } 77 | return false, nil 78 | } 79 | 80 | func Commit(tx *Tx) error { 81 | _, err := commit(tx) 82 | return err 83 | } 84 | 85 | // CommitWithStorageDiff commits the transaction and applies the storage diff, only if the transaction is not inherited. 86 | func CommitWithStorageDiff(ctx context.Context, tx *Tx, l logging.Logger, uc UserClient) error { 87 | commited, err := commit(tx) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if !commited { 93 | return nil 94 | } 95 | 96 | if err := uc.ApplyStorageDiff(ctx, tx.storageDiff); err != nil { 97 | l.Error("Failed to apply storage diff", "error", err) 98 | } 99 | 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "flag" 6 | "github.com/cloudreve/Cloudreve/v4/cmd" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 8 | ) 9 | 10 | var ( 11 | isEject bool 12 | confPath string 13 | scriptName string 14 | ) 15 | 16 | func init() { 17 | flag.BoolVar(&util.UseWorkingDir, "use-working-dir", false, "Use working directory, instead of executable directory") 18 | flag.StringVar(&confPath, "c", util.RelativePath("conf.ini"), "Path to the config file.") 19 | flag.StringVar(&scriptName, "database-script", "", "Name of database util script.") 20 | //flag.Parse() 21 | 22 | //staticFS = bootstrap.NewFS(staticZip) 23 | //bootstrap.Init(confPath, staticFS) 24 | } 25 | 26 | func main() { 27 | cmd.Execute() 28 | return 29 | // 关闭数据库连接 30 | 31 | //if scriptName != "" { 32 | // // 开始运行助手数据库脚本 33 | // bootstrap.RunScript(scriptName) 34 | // return 35 | //} 36 | } 37 | -------------------------------------------------------------------------------- /middleware/cluster.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 7 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/cluster" 9 | "github.com/cloudreve/Cloudreve/v4/pkg/downloader" 10 | "github.com/cloudreve/Cloudreve/v4/pkg/request" 11 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 12 | "github.com/cloudreve/Cloudreve/v4/routers/controllers" 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | type SlaveNodeSettingGetter interface { 17 | // GetNodeSetting returns the node settings and its hash 18 | GetNodeSetting() (*types.NodeSetting, string) 19 | } 20 | 21 | var downloaderPool = sync.Map{} 22 | 23 | // PrepareSlaveDownloader creates or resume a downloader based on input node settings 24 | func PrepareSlaveDownloader(dep dependency.Dep, ctxKey interface{}) gin.HandlerFunc { 25 | return func(c *gin.Context) { 26 | nodeSettings, hash := controllers.ParametersFromContext[SlaveNodeSettingGetter](c, ctxKey).GetNodeSetting() 27 | 28 | // try to get downloader from pool 29 | if d, ok := downloaderPool.Load(hash); ok { 30 | c.Set(downloader.DownloaderCtxKey, d) 31 | c.Next() 32 | return 33 | } 34 | 35 | // create a new downloader 36 | d, err := cluster.NewDownloader(c, dep.RequestClient(request.WithContext(c), request.WithLogger(dep.Logger())), dep.SettingProvider(), nodeSettings) 37 | if err != nil { 38 | c.JSON(200, serializer.ParamErr(c, "Failed to create downloader", err)) 39 | c.Abort() 40 | return 41 | } 42 | 43 | // save downloader to pool 44 | downloaderPool.Store(hash, d) 45 | c.Set(downloader.DownloaderCtxKey, d) 46 | c.Next() 47 | } 48 | } 49 | 50 | func SlaveRPCSignRequired() gin.HandlerFunc { 51 | return func(c *gin.Context) { 52 | nodeId := cluster.NodeIdFromContext(c) 53 | if nodeId == 0 { 54 | c.JSON(200, serializer.ParamErr(c, "Unknown node ID", nil)) 55 | c.Abort() 56 | return 57 | } 58 | 59 | np, err := dependency.FromContext(c).NodePool(c) 60 | if err != nil { 61 | c.JSON(200, serializer.NewError(serializer.CodeInternalSetting, "Failed to get node pool", err)) 62 | c.Abort() 63 | return 64 | } 65 | 66 | slaveNode, err := np.Get(c, types.NodeCapabilityNone, nodeId) 67 | if slaveNode == nil || slaveNode.IsMaster() { 68 | c.JSON(200, serializer.ParamErr(c, "Unknown node ID", err)) 69 | c.Abort() 70 | return 71 | } 72 | 73 | SignRequired(slaveNode.AuthInstance())(c) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /middleware/file.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 9 | "github.com/cloudreve/Cloudreve/v4/routers/controllers" 10 | "github.com/gin-gonic/gin" 11 | "github.com/gofrs/uuid" 12 | ) 13 | 14 | // UrisService is a wrapper for service supports batch file operations 15 | type UrisService interface { 16 | GetUris() []string 17 | } 18 | 19 | // ValidateBatchFileCount validates if the batch file count is within the limit 20 | func ValidateBatchFileCount(dep dependency.Dep, ctxKey interface{}) gin.HandlerFunc { 21 | settings := dep.SettingProvider() 22 | return func(c *gin.Context) { 23 | uris := controllers.ParametersFromContext[UrisService](c, ctxKey) 24 | limit := settings.MaxBatchedFile(c) 25 | if len((uris).GetUris()) > limit { 26 | c.JSON(200, serializer.ErrWithDetails( 27 | c, 28 | serializer.CodeTooManyUris, 29 | fmt.Sprintf("Maximum allowed batch size: %d", limit), 30 | nil, 31 | )) 32 | c.Abort() 33 | return 34 | } 35 | 36 | c.Next() 37 | } 38 | } 39 | 40 | // ContextHint parses the context hint header and set it to context 41 | func ContextHint() gin.HandlerFunc { 42 | return func(c *gin.Context) { 43 | if c.GetHeader(dbfs.ContextHintHeader) != "" { 44 | util.WithValue(c, dbfs.ContextHintCtxKey{}, uuid.FromStringOrNil(c.GetHeader(dbfs.ContextHintHeader))) 45 | } 46 | 47 | c.Next() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /middleware/frontend.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 5 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 6 | "github.com/gin-gonic/gin" 7 | "io" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // FrontendFileHandler 前端静态文件处理 13 | func FrontendFileHandler(dep dependency.Dep) gin.HandlerFunc { 14 | fs := dep.ServerStaticFS() 15 | l := dep.Logger() 16 | 17 | ignoreFunc := func(c *gin.Context) { 18 | c.Next() 19 | } 20 | 21 | if fs == nil { 22 | return ignoreFunc 23 | } 24 | 25 | // 读取index.html 26 | file, err := fs.Open("/index.html") 27 | if err != nil { 28 | l.Warning("Static file \"index.html\" does not exist, it might affect the display of the homepage.") 29 | return ignoreFunc 30 | } 31 | 32 | fileContentBytes, err := io.ReadAll(file) 33 | if err != nil { 34 | l.Warning("Cannot read static file \"index.html\", it might affect the display of the homepage.") 35 | return ignoreFunc 36 | } 37 | fileContent := string(fileContentBytes) 38 | 39 | fileServer := http.FileServer(fs) 40 | return func(c *gin.Context) { 41 | path := c.Request.URL.Path 42 | 43 | // Skipping routers handled by backend 44 | if strings.HasPrefix(path, "/api") || 45 | strings.HasPrefix(path, "/dav") || 46 | strings.HasPrefix(path, "/f/") || 47 | strings.HasPrefix(path, "/s/") || 48 | path == "/manifest.json" { 49 | c.Next() 50 | return 51 | } 52 | 53 | // 不存在的路径和index.html均返回index.html 54 | if (path == "/index.html") || (path == "/") || !fs.Exists("/", path) { 55 | // 读取、替换站点设置 56 | settingClient := dep.SettingProvider() 57 | siteBasic := settingClient.SiteBasic(c) 58 | pwaOpts := settingClient.PWA(c) 59 | theme := settingClient.Theme(c) 60 | finalHTML := util.Replace(map[string]string{ 61 | "{siteName}": siteBasic.Name, 62 | "{siteDes}": siteBasic.Description, 63 | "{siteScript}": siteBasic.Script, 64 | "{pwa_small_icon}": pwaOpts.SmallIcon, 65 | "{pwa_medium_icon}": pwaOpts.MediumIcon, 66 | "var(--defaultThemeColor)": theme.DefaultTheme, 67 | }, fileContent) 68 | 69 | c.Header("Content-Type", "text/html") 70 | c.Header("Cache-Control", "public, no-cache") 71 | c.String(200, finalHTML) 72 | c.Abort() 73 | return 74 | } 75 | 76 | if path == "/sw.js" || strings.HasPrefix(path, "/locales/") { 77 | c.Header("Cache-Control", "public, no-cache") 78 | } else if strings.HasPrefix(path, "/assets/") { 79 | c.Header("Cache-Control", "public, max-age=31536000") 80 | } 81 | 82 | // 存在的静态文件 83 | fileServer.ServeHTTP(c.Writer, c.Request) 84 | c.Abort() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /middleware/mock.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | // SessionMock 测试时模拟Session 9 | var SessionMock = make(map[string]interface{}) 10 | 11 | // ContextMock 测试时模拟Context 12 | var ContextMock = make(map[string]interface{}) 13 | 14 | // MockHelper 单元测试助手中间件 15 | func MockHelper() gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | // 将SessionMock写入会话 18 | util.SetSession(c, SessionMock) 19 | for key, value := range ContextMock { 20 | c.Set(key, value) 21 | } 22 | c.Next() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /middleware/session.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 5 | "github.com/cloudreve/Cloudreve/v4/pkg/sessionstore" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/cloudreve/Cloudreve/v4/pkg/conf" 10 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 11 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 12 | "github.com/gin-contrib/sessions" 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | // Store session存储 17 | var Store sessions.Store 18 | 19 | const SessionName = "cloudreve-session" 20 | 21 | // Session 初始化session 22 | func Session(dep dependency.Dep) gin.HandlerFunc { 23 | Store = sessionstore.NewStore(dep.KV(), []byte(dep.ConfigProvider().System().SessionSecret)) 24 | sameSiteMode := http.SameSiteDefaultMode 25 | switch strings.ToLower(conf.CORSConfig.SameSite) { 26 | case "default": 27 | sameSiteMode = http.SameSiteDefaultMode 28 | case "none": 29 | sameSiteMode = http.SameSiteNoneMode 30 | case "strict": 31 | sameSiteMode = http.SameSiteStrictMode 32 | case "lax": 33 | sameSiteMode = http.SameSiteLaxMode 34 | } 35 | 36 | // Also set Secure: true if using SSL, you should though 37 | Store.Options(sessions.Options{ 38 | HttpOnly: true, 39 | MaxAge: 60 * 86400, 40 | Path: "/", 41 | SameSite: sameSiteMode, 42 | Secure: conf.CORSConfig.Secure, 43 | }) 44 | 45 | return sessions.Sessions(SessionName, Store) 46 | } 47 | 48 | // CSRFInit 初始化CSRF标记 49 | func CSRFInit() gin.HandlerFunc { 50 | return func(c *gin.Context) { 51 | util.SetSession(c, map[string]interface{}{"CSRF": true}) 52 | c.Next() 53 | } 54 | } 55 | 56 | // CSRFCheck 检查CSRF标记 57 | func CSRFCheck() gin.HandlerFunc { 58 | return func(c *gin.Context) { 59 | if check, ok := util.GetSession(c, "CSRF").(bool); ok && check { 60 | c.Next() 61 | return 62 | } 63 | 64 | c.JSON(200, serializer.ErrDeprecated(serializer.CodeNoPermissionErr, "Invalid origin", nil)) 65 | c.Abort() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /middleware/wopi.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 5 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/hashid" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/setting" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 9 | "github.com/cloudreve/Cloudreve/v4/pkg/wopi" 10 | "github.com/gin-gonic/gin" 11 | "net/http" 12 | "strings" 13 | ) 14 | 15 | // WopiWriteAccess validates if write access is obtained. 16 | func WopiWriteAccess() gin.HandlerFunc { 17 | return func(c *gin.Context) { 18 | session := c.MustGet(wopi.WopiSessionCtx).(*wopi.SessionCache) 19 | if session.Action != wopi.ActionEdit { 20 | c.Status(http.StatusNotFound) 21 | c.Header(wopi.ServerErrorHeader, "read-only access") 22 | c.Abort() 23 | return 24 | } 25 | 26 | c.Next() 27 | } 28 | } 29 | 30 | func ViewerSessionValidation() gin.HandlerFunc { 31 | return func(c *gin.Context) { 32 | dep := dependency.FromContext(c) 33 | store := dep.KV() 34 | settings := dep.SettingProvider() 35 | 36 | accessToken := strings.Split(c.Query(wopi.AccessTokenQuery), ".") 37 | if len(accessToken) != 2 { 38 | c.Status(http.StatusForbidden) 39 | c.Header(wopi.ServerErrorHeader, "malformed access token") 40 | c.Abort() 41 | return 42 | } 43 | 44 | sessionRaw, exist := store.Get(manager.ViewerSessionCachePrefix + accessToken[0]) 45 | if !exist { 46 | c.Status(http.StatusForbidden) 47 | c.Header(wopi.ServerErrorHeader, "invalid access token") 48 | c.Abort() 49 | return 50 | } 51 | 52 | session := sessionRaw.(manager.ViewerSessionCache) 53 | if err := SetUserCtx(c, session.UserID); err != nil { 54 | c.Status(http.StatusInternalServerError) 55 | c.Header(wopi.ServerErrorHeader, "user not found") 56 | c.Abort() 57 | return 58 | } 59 | 60 | fileId := hashid.FromContext(c) 61 | if fileId != session.FileID { 62 | c.Status(http.StatusForbidden) 63 | c.Header(wopi.ServerErrorHeader, "invalid file") 64 | c.Abort() 65 | return 66 | } 67 | 68 | // Check if the viewer is still available 69 | viewers := settings.FileViewers(c) 70 | var v *setting.Viewer 71 | for _, group := range viewers { 72 | for _, viewer := range group.Viewers { 73 | if viewer.ID == session.ViewerID && !viewer.Disabled { 74 | v = &viewer 75 | break 76 | } 77 | } 78 | 79 | if v != nil { 80 | break 81 | } 82 | } 83 | 84 | if v == nil { 85 | c.Status(http.StatusInternalServerError) 86 | c.Header(wopi.ServerErrorHeader, "viewer not found") 87 | c.Abort() 88 | return 89 | } 90 | 91 | util.WithValue(c, manager.ViewerCtx{}, v) 92 | util.WithValue(c, manager.ViewerSessionCacheCtx{}, &session) 93 | c.Next() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg/auth/hmac.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "io" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 13 | ) 14 | 15 | // HMACAuth HMAC算法鉴权 16 | type HMACAuth struct { 17 | SecretKey []byte 18 | } 19 | 20 | // Sign 对给定Body生成expires后失效的签名,expires为过期时间戳, 21 | // 填写为0表示不限制有效期 22 | func (auth HMACAuth) Sign(body string, expires int64) string { 23 | h := hmac.New(sha256.New, auth.SecretKey) 24 | expireTimeStamp := strconv.FormatInt(expires, 10) 25 | _, err := io.WriteString(h, body+":"+expireTimeStamp) 26 | if err != nil { 27 | return "" 28 | } 29 | 30 | return base64.URLEncoding.EncodeToString(h.Sum(nil)) + ":" + expireTimeStamp 31 | } 32 | 33 | // Check 对给定Body和Sign进行鉴权,包括对expires的检查 34 | func (auth HMACAuth) Check(body string, sign string) error { 35 | signSlice := strings.Split(sign, ":") 36 | // 如果未携带expires字段 37 | if signSlice[len(signSlice)-1] == "" { 38 | return ErrExpiresMissing 39 | } 40 | 41 | // 验证是否过期 42 | expires, err := strconv.ParseInt(signSlice[len(signSlice)-1], 10, 64) 43 | if err != nil { 44 | return serializer.NewError(serializer.CodeInvalidSign, "sign expired", nil) 45 | } 46 | // 如果签名过期 47 | if expires < time.Now().Unix() && expires != 0 { 48 | return ErrExpired 49 | } 50 | 51 | // 验证签名 52 | if auth.Sign(body, expires) != sign { 53 | return serializer.NewError(serializer.CodeInvalidSign, "invalid sign", nil) 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/auth/requestinfo/requestinfo.go: -------------------------------------------------------------------------------- 1 | package requestinfo 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // RequestInfoCtx context key for RequestInfo 8 | type RequestInfoCtx struct{} 9 | 10 | // RequestInfoFromContext retrieves RequestInfo from context 11 | func RequestInfoFromContext(ctx context.Context) *RequestInfo { 12 | v, ok := ctx.Value(RequestInfoCtx{}).(*RequestInfo) 13 | if !ok { 14 | return nil 15 | } 16 | 17 | return v 18 | } 19 | 20 | // RequestInfo store request info for audit 21 | type RequestInfo struct { 22 | Host string 23 | IP string 24 | UserAgent string 25 | } 26 | -------------------------------------------------------------------------------- /pkg/balancer/balancer.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | type Balancer interface { 4 | NextPeer(nodes interface{}) (error, interface{}) 5 | } 6 | 7 | // NewBalancer 根据策略标识返回新的负载均衡器 8 | func NewBalancer(strategy string) Balancer { 9 | switch strategy { 10 | case "RoundRobin": 11 | return &RoundRobin{} 12 | default: 13 | return &RoundRobin{} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/balancer/balancer_test.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestNewBalancer(t *testing.T) { 9 | a := assert.New(t) 10 | a.NotNil(NewBalancer("")) 11 | a.IsType(&RoundRobin{}, NewBalancer("RoundRobin")) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/balancer/errors.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInputNotSlice = errors.New("Input value is not silice") 7 | ErrNoAvaliableNode = errors.New("No nodes avaliable") 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/balancer/roundrobin.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | import ( 4 | "reflect" 5 | "sync/atomic" 6 | ) 7 | 8 | type RoundRobin struct { 9 | current uint64 10 | } 11 | 12 | // NextPeer 返回轮盘的下一节点 13 | func (r *RoundRobin) NextPeer(nodes interface{}) (error, interface{}) { 14 | v := reflect.ValueOf(nodes) 15 | if v.Kind() != reflect.Slice { 16 | return ErrInputNotSlice, nil 17 | } 18 | 19 | if v.Len() == 0 { 20 | return ErrNoAvaliableNode, nil 21 | } 22 | 23 | next := r.NextIndex(v.Len()) 24 | return nil, v.Index(next).Interface() 25 | } 26 | 27 | // NextIndex 返回下一个节点下标 28 | func (r *RoundRobin) NextIndex(total int) int { 29 | return int(atomic.AddUint64(&r.current, uint64(1)) % uint64(total)) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/balancer/roundrobin_test.go: -------------------------------------------------------------------------------- 1 | package balancer 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestRoundRobin_NextIndex(t *testing.T) { 9 | a := assert.New(t) 10 | r := &RoundRobin{} 11 | total := 5 12 | for i := 1; i < total; i++ { 13 | a.Equal(i, r.NextIndex(total)) 14 | } 15 | for i := 0; i < total; i++ { 16 | a.Equal(i, r.NextIndex(total)) 17 | } 18 | } 19 | 20 | func TestRoundRobin_NextPeer(t *testing.T) { 21 | a := assert.New(t) 22 | r := &RoundRobin{} 23 | 24 | // not slice 25 | { 26 | err, _ := r.NextPeer("s") 27 | a.Equal(ErrInputNotSlice, err) 28 | } 29 | 30 | // no nodes 31 | { 32 | err, _ := r.NextPeer([]string{}) 33 | a.Equal(ErrNoAvaliableNode, err) 34 | } 35 | 36 | // pass 37 | { 38 | err, res := r.NextPeer([]string{"a"}) 39 | a.NoError(err) 40 | a.Equal("a", res.(string)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/boolset/boolset.go: -------------------------------------------------------------------------------- 1 | package boolset 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/base64" 6 | "errors" 7 | "golang.org/x/exp/constraints" 8 | ) 9 | 10 | var ( 11 | ErrValueNotSupported = errors.New("value not supported") 12 | ) 13 | 14 | type BooleanSet []byte 15 | 16 | // FromString convert from base64 encoded boolset. 17 | func FromString(data string) (*BooleanSet, error) { 18 | raw, err := base64.StdEncoding.DecodeString(data) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | b := BooleanSet(raw) 24 | return &b, nil 25 | } 26 | 27 | func (b *BooleanSet) UnmarshalBinary(data []byte) error { 28 | *b = make(BooleanSet, len(data)) 29 | copy(*b, data) 30 | return nil 31 | } 32 | 33 | func (b *BooleanSet) MarshalBinary() (data []byte, err error) { 34 | return *b, nil 35 | } 36 | 37 | func (b *BooleanSet) String() (data string, err error) { 38 | raw, err := b.MarshalBinary() 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | return base64.StdEncoding.EncodeToString(raw), nil 44 | } 45 | 46 | func (b *BooleanSet) Enabled(flag int) bool { 47 | if flag >= len(*b)*8 { 48 | return false 49 | } 50 | 51 | return (*b)[flag/8]&(1< c.Max { 30 | return false 31 | } 32 | 33 | var e *RetryableError 34 | if errors.As(err, &e) && e.RetryAfter > 0 { 35 | util.Log().Warning("Retryable error %q occurs in backoff, will sleep after %s.", e, e.RetryAfter) 36 | time.Sleep(e.RetryAfter) 37 | } else { 38 | time.Sleep(c.Sleep) 39 | } 40 | 41 | return true 42 | } 43 | 44 | func (c *ConstantBackoff) Reset() { 45 | c.tried = 0 46 | } 47 | 48 | type RetryableError struct { 49 | Err error 50 | RetryAfter time.Duration 51 | } 52 | 53 | // NewRetryableErrorFromHeader constructs a new RetryableError from http response header 54 | // and existing error. 55 | func NewRetryableErrorFromHeader(err error, header http.Header) *RetryableError { 56 | retryAfter := header.Get("retry-after") 57 | if retryAfter == "" { 58 | retryAfter = "0" 59 | } 60 | 61 | res := &RetryableError{ 62 | Err: err, 63 | } 64 | 65 | if retryAfterSecond, err := strconv.ParseInt(retryAfter, 10, 64); err == nil { 66 | res.RetryAfter = time.Duration(retryAfterSecond) * time.Second 67 | } 68 | 69 | return res 70 | } 71 | 72 | func (e *RetryableError) Error() string { 73 | return fmt.Sprintf("retryable error with retry-after=%s: %s", e.RetryAfter, e.Err) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/filemanager/driver/local/entity.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/ent" 5 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 8 | "github.com/gofrs/uuid" 9 | "os" 10 | "time" 11 | ) 12 | 13 | // NewLocalFileEntity creates a new local file entity. 14 | func NewLocalFileEntity(t types.EntityType, src string) (fs.Entity, error) { 15 | info, err := os.Stat(util.RelativePath(src)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &localFileEntity{ 21 | t: t, 22 | src: src, 23 | size: info.Size(), 24 | }, nil 25 | } 26 | 27 | type localFileEntity struct { 28 | t types.EntityType 29 | src string 30 | size int64 31 | } 32 | 33 | func (l *localFileEntity) ID() int { 34 | return 0 35 | } 36 | 37 | func (l *localFileEntity) Type() types.EntityType { 38 | return l.t 39 | } 40 | 41 | func (l *localFileEntity) Size() int64 { 42 | return l.size 43 | } 44 | 45 | func (l *localFileEntity) UpdatedAt() time.Time { 46 | return time.Now() 47 | } 48 | 49 | func (l *localFileEntity) CreatedAt() time.Time { 50 | return time.Now() 51 | } 52 | 53 | func (l *localFileEntity) CreatedBy() *ent.User { 54 | return nil 55 | } 56 | 57 | func (l *localFileEntity) Source() string { 58 | return l.src 59 | } 60 | 61 | func (l *localFileEntity) ReferenceCount() int { 62 | return 1 63 | } 64 | 65 | func (l *localFileEntity) PolicyID() int { 66 | return 0 67 | } 68 | 69 | func (l *localFileEntity) UploadSessionID() *uuid.UUID { 70 | return nil 71 | } 72 | 73 | func (l *localFileEntity) Model() *ent.Entity { 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/filemanager/driver/local/fallocate.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin 2 | // +build !linux,!darwin 3 | 4 | package local 5 | 6 | import "os" 7 | 8 | // No-op on non-Linux/Darwin platforms. 9 | func Fallocate(file *os.File, offset int64, length int64) error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /pkg/filemanager/driver/local/fallocate_darwin.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "unsafe" 7 | ) 8 | 9 | func Fallocate(file *os.File, offset int64, length int64) error { 10 | var fst syscall.Fstore_t 11 | 12 | fst.Flags = syscall.F_ALLOCATECONTIG 13 | fst.Posmode = syscall.F_PREALLOCATE 14 | fst.Offset = 0 15 | fst.Length = offset + length 16 | fst.Bytesalloc = 0 17 | 18 | // Check https://lists.apple.com/archives/darwin-dev/2007/Dec/msg00040.html 19 | _, _, err := syscall.Syscall(syscall.SYS_FCNTL, file.Fd(), syscall.F_PREALLOCATE, uintptr(unsafe.Pointer(&fst))) 20 | if err != syscall.Errno(0x0) { 21 | fst.Flags = syscall.F_ALLOCATEALL 22 | // Ignore the return value 23 | _, _, _ = syscall.Syscall(syscall.SYS_FCNTL, file.Fd(), syscall.F_PREALLOCATE, uintptr(unsafe.Pointer(&fst))) 24 | } 25 | 26 | return syscall.Ftruncate(int(file.Fd()), fst.Length) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/filemanager/driver/local/fallocate_linux.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | ) 7 | 8 | func Fallocate(file *os.File, offset int64, length int64) error { 9 | if length == 0 { 10 | return nil 11 | } 12 | 13 | return syscall.Fallocate(int(file.Fd()), 0, offset, length) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/filemanager/driver/onedrive/options.go: -------------------------------------------------------------------------------- 1 | package onedrive 2 | 3 | import "time" 4 | 5 | // Option 发送请求的额外设置 6 | type Option interface { 7 | apply(*options) 8 | } 9 | 10 | type options struct { 11 | redirect string 12 | code string 13 | refreshToken string 14 | conflictBehavior string 15 | expires time.Time 16 | useDriverResource bool 17 | } 18 | 19 | type optionFunc func(*options) 20 | 21 | // WithCode 设置接口Code 22 | func WithCode(t string) Option { 23 | return optionFunc(func(o *options) { 24 | o.code = t 25 | }) 26 | } 27 | 28 | // WithRefreshToken 设置接口RefreshToken 29 | func WithRefreshToken(t string) Option { 30 | return optionFunc(func(o *options) { 31 | o.refreshToken = t 32 | }) 33 | } 34 | 35 | // WithConflictBehavior 设置文件重名后的处理方式 36 | func WithConflictBehavior(t string) Option { 37 | return optionFunc(func(o *options) { 38 | o.conflictBehavior = t 39 | }) 40 | } 41 | 42 | // WithConflictBehavior 设置文件重名后的处理方式 43 | func WithDriverResource(t bool) Option { 44 | return optionFunc(func(o *options) { 45 | o.useDriverResource = t 46 | }) 47 | } 48 | 49 | func (f optionFunc) apply(o *options) { 50 | f(o) 51 | } 52 | 53 | func newDefaultOption() *options { 54 | return &options{ 55 | conflictBehavior: "fail", 56 | useDriverResource: true, 57 | expires: time.Now().UTC().Add(time.Duration(1) * time.Hour), 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/filemanager/driver/util.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "path" 7 | "strings" 8 | 9 | "github.com/cloudreve/Cloudreve/v4/ent" 10 | ) 11 | 12 | func ApplyProxyIfNeeded(policy *ent.StoragePolicy, srcUrl *url.URL) (*url.URL, error) { 13 | // For custom proxy, generate a new proxyed URL: 14 | // [Proxy Scheme][Proxy Host][Proxy Port][ProxyPath + OriginSrcPath][OriginSrcQuery + ProxyQuery] 15 | if policy.Settings.CustomProxy { 16 | proxy, err := url.Parse(policy.Settings.ProxyServer) 17 | if err != nil { 18 | return nil, fmt.Errorf("failed to parse proxy URL: %w", err) 19 | } 20 | if proxy.Path != "" && proxy.Path != "/" { 21 | proxy.Path = path.Join(proxy.Path, strings.TrimPrefix(srcUrl.Path, "/")) 22 | } else { 23 | proxy.RawPath = srcUrl.RawPath 24 | proxy.Path = srcUrl.Path 25 | } 26 | q := proxy.Query() 27 | if len(q) == 0 { 28 | proxy.RawQuery = srcUrl.RawQuery 29 | } else { 30 | // Merge query parameters 31 | srcQ := srcUrl.Query() 32 | for k, _ := range srcQ { 33 | q.Set(k, srcQ.Get(k)) 34 | } 35 | 36 | proxy.RawQuery = q.Encode() 37 | } 38 | 39 | srcUrl = proxy 40 | } 41 | 42 | return srcUrl, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/filemanager/fs/dbfs/global.go: -------------------------------------------------------------------------------- 1 | package dbfs 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudreve/Cloudreve/v4/ent" 6 | "github.com/cloudreve/Cloudreve/v4/inventory" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" 8 | "github.com/samber/lo" 9 | ) 10 | 11 | func (f *DBFS) StaleEntities(ctx context.Context, entities ...int) ([]fs.Entity, error) { 12 | res, err := f.fileClient.StaleEntities(ctx, entities...) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | return lo.Map(res, func(e *ent.Entity, i int) fs.Entity { 18 | return fs.NewEntity(e) 19 | }), nil 20 | } 21 | 22 | func (f *DBFS) AllFilesInTrashBin(ctx context.Context, opts ...fs.Option) (*fs.ListFileResult, error) { 23 | o := newDbfsOption() 24 | for _, opt := range opts { 25 | o.apply(opt) 26 | } 27 | 28 | navigator, err := f.getNavigator(ctx, newTrashUri(""), NavigatorCapabilityListChildren) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | ctx = context.WithValue(ctx, inventory.LoadFilePublicMetadata{}, true) 34 | children, err := navigator.Children(ctx, nil, &ListArgs{ 35 | Page: &inventory.PaginationArgs{ 36 | Page: o.FsOption.Page, 37 | PageSize: o.PageSize, 38 | OrderBy: o.OrderBy, 39 | Order: inventory.OrderDirection(o.OrderDirection), 40 | UseCursorPagination: o.useCursorPagination, 41 | PageToken: o.pageToken, 42 | }, 43 | }) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return &fs.ListFileResult{ 49 | Files: lo.Map(children.Files, func(item *File, index int) fs.File { 50 | return item 51 | }), 52 | Pagination: children.Pagination, 53 | RecursionLimitReached: children.RecursionLimitReached, 54 | }, nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/filemanager/fs/dbfs/validator.go: -------------------------------------------------------------------------------- 1 | package dbfs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloudreve/Cloudreve/v4/ent" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 9 | "strings" 10 | ) 11 | 12 | const MaxFileNameLength = 256 13 | 14 | // validateFileName validates the file name. 15 | func validateFileName(name string) error { 16 | if len(name) >= MaxFileNameLength || len(name) == 0 { 17 | return fmt.Errorf("length of name must be between 1 and 255") 18 | } 19 | 20 | if strings.ContainsAny(name, "\\/:*?\"<>|") { 21 | return fmt.Errorf("name contains illegal characters") 22 | } 23 | 24 | if name == "." || name == ".." { 25 | return fmt.Errorf("name cannot be only dot") 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // validateExtension validates the file extension. 32 | func validateExtension(name string, policy *ent.StoragePolicy) error { 33 | // 不需要验证 34 | if len(policy.Settings.FileType) == 0 { 35 | return nil 36 | } 37 | 38 | if !util.IsInExtensionList(policy.Settings.FileType, name) { 39 | return fmt.Errorf("file extension is not allowed") 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // validateFileSize validates the file size. 46 | func validateFileSize(size int64, policy *ent.StoragePolicy) error { 47 | if policy.MaxSize == 0 { 48 | return nil 49 | } else if size > policy.MaxSize { 50 | return fs.ErrFileSizeTooBig 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // validateNewFile validates the upload request. 57 | func validateNewFile(fileName string, size int64, policy *ent.StoragePolicy) error { 58 | if err := validateFileName(fileName); err != nil { 59 | return err 60 | } 61 | 62 | if err := validateExtension(fileName, policy); err != nil { 63 | return err 64 | } 65 | 66 | if err := validateFileSize(size, policy); err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (f *DBFS) validateUserCapacity(ctx context.Context, size int64, u *ent.User) error { 74 | capacity, err := f.Capacity(ctx, u) 75 | if err != nil { 76 | return fmt.Errorf("failed to get user capacity: %s", err) 77 | } 78 | 79 | return f.validateUserCapacityRaw(ctx, size, capacity) 80 | } 81 | 82 | // validateUserCapacityRaw validates the user capacity, but does not fetch the capacity. 83 | func (f *DBFS) validateUserCapacityRaw(ctx context.Context, size int64, capacity *fs.Capacity) error { 84 | if capacity.Used+size > capacity.Total { 85 | return fs.ErrInsufficientCapacity 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /pkg/filemanager/fs/mime/mime.go: -------------------------------------------------------------------------------- 1 | package mime 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/logging" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/setting" 8 | "mime" 9 | "path" 10 | ) 11 | 12 | type MimeDetector interface { 13 | // TypeByName returns the mime type by file name. 14 | TypeByName(ext string) string 15 | } 16 | 17 | type mimeDetector struct { 18 | mapping map[string]string 19 | } 20 | 21 | func NewMimeDetector(ctx context.Context, settings setting.Provider, l logging.Logger) MimeDetector { 22 | mappingStr := settings.MimeMapping(ctx) 23 | mapping := make(map[string]string) 24 | if err := json.Unmarshal([]byte(mappingStr), &mapping); err != nil { 25 | l.Error("Failed to unmarshal mime mapping: %s, fallback to empty mapping", err) 26 | } 27 | 28 | return &mimeDetector{ 29 | mapping: mapping, 30 | } 31 | } 32 | 33 | func (d *mimeDetector) TypeByName(p string) string { 34 | ext := path.Ext(p) 35 | if m, ok := d.mapping[ext]; ok { 36 | return m 37 | } 38 | 39 | return mime.TypeByExtension(ext) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/filemanager/lock/memlock_test.go: -------------------------------------------------------------------------------- 1 | package lock 2 | -------------------------------------------------------------------------------- /pkg/filemanager/manager/viewer.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "context" 5 | "encoding/gob" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 10 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" 11 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" 12 | "github.com/cloudreve/Cloudreve/v4/pkg/setting" 13 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 14 | "github.com/gofrs/uuid" 15 | ) 16 | 17 | type ( 18 | ViewerSession struct { 19 | ID string `json:"id"` 20 | AccessToken string `json:"access_token"` 21 | Expires int64 `json:"expires"` 22 | File fs.File `json:"-"` 23 | } 24 | ViewerSessionCache struct { 25 | ID string 26 | Uri string 27 | UserID int 28 | FileID int 29 | ViewerID string 30 | Version string 31 | Token string 32 | } 33 | ViewerSessionCacheCtx struct{} 34 | ViewerCtx struct{} 35 | ) 36 | 37 | const ( 38 | ViewerSessionCachePrefix = "viewer_session_" 39 | 40 | sessionExpiresPadding = 10 41 | ) 42 | 43 | func init() { 44 | gob.Register(ViewerSessionCache{}) 45 | } 46 | 47 | func (m *manager) CreateViewerSession(ctx context.Context, uri *fs.URI, version string, viewer *setting.Viewer) (*ViewerSession, error) { 48 | file, err := m.fs.Get(ctx, uri, dbfs.WithFileEntities(), dbfs.WithNotRoot()) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | versionType := types.EntityTypeVersion 54 | found, desired := fs.FindDesiredEntity(file, version, m.hasher, &versionType) 55 | if !found { 56 | return nil, fs.ErrEntityNotExist 57 | } 58 | 59 | if desired.Size() > m.settings.MaxOnlineEditSize(ctx) { 60 | return nil, fs.ErrFileSizeTooBig 61 | } 62 | 63 | sessionID := uuid.Must(uuid.NewV4()).String() 64 | token := util.RandStringRunes(128) 65 | sessionCache := &ViewerSessionCache{ 66 | ID: sessionID, 67 | Uri: file.Uri(false).String(), 68 | UserID: m.user.ID, 69 | ViewerID: viewer.ID, 70 | FileID: file.ID(), 71 | Version: version, 72 | Token: fmt.Sprintf("%s.%s", sessionID, token), 73 | } 74 | ttl := m.settings.ViewerSessionTTL(ctx) 75 | if err := m.kv.Set(ViewerSessionCachePrefix+sessionID, *sessionCache, ttl); err != nil { 76 | return nil, err 77 | } 78 | 79 | return &ViewerSession{ 80 | File: file, 81 | ID: sessionID, 82 | AccessToken: sessionCache.Token, 83 | Expires: time.Now().Add(time.Duration(ttl-sessionExpiresPadding) * time.Second).UnixMilli(), 84 | }, nil 85 | } 86 | 87 | func ViewerSessionFromContext(ctx context.Context) *ViewerSessionCache { 88 | return ctx.Value(ViewerSessionCacheCtx{}).(*ViewerSessionCache) 89 | } 90 | 91 | func ViewerFromContext(ctx context.Context) *setting.Viewer { 92 | return ctx.Value(ViewerCtx{}).(*setting.Viewer) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/filemanager/workflows/worfklows.go: -------------------------------------------------------------------------------- 1 | package workflows 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 11 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 12 | "github.com/cloudreve/Cloudreve/v4/pkg/cluster" 13 | "github.com/cloudreve/Cloudreve/v4/pkg/queue" 14 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 15 | ) 16 | 17 | const ( 18 | TaskTempPath = "fm_workflows" 19 | slaveProgressRefreshInterval = 5 * time.Second 20 | ) 21 | 22 | type NodeState struct { 23 | NodeID int `json:"node_id"` 24 | 25 | progress queue.Progresses 26 | } 27 | 28 | // allocateNode allocates a node for the task. 29 | func allocateNode(ctx context.Context, dep dependency.Dep, state *NodeState, capability types.NodeCapability) (cluster.Node, error) { 30 | np, err := dep.NodePool(ctx) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to get node pool: %w", err) 33 | } 34 | 35 | node, err := np.Get(ctx, capability, state.NodeID) 36 | if err != nil { 37 | return nil, fmt.Errorf("failed to get node: %w", err) 38 | } 39 | 40 | state.NodeID = node.ID() 41 | return node, nil 42 | } 43 | 44 | // prepareSlaveTaskCtx prepares the context for the slave task. 45 | func prepareSlaveTaskCtx(ctx context.Context, props *types.SlaveTaskProps) context.Context { 46 | ctx = context.WithValue(ctx, cluster.SlaveNodeIDCtx{}, strconv.Itoa(props.NodeID)) 47 | ctx = context.WithValue(ctx, cluster.MasterSiteUrlCtx{}, props.MasterSiteURl) 48 | ctx = context.WithValue(ctx, cluster.MasterSiteVersionCtx{}, props.MasterSiteVersion) 49 | ctx = context.WithValue(ctx, cluster.MasterSiteIDCtx{}, props.MasterSiteID) 50 | return ctx 51 | } 52 | 53 | func prepareTempFolder(ctx context.Context, dep dependency.Dep, t queue.Task) (string, error) { 54 | settings := dep.SettingProvider() 55 | tempPath := util.DataPath(path.Join(settings.TempPath(ctx), TaskTempPath, strconv.Itoa(t.ID()))) 56 | if err := util.CreatNestedFolder(tempPath); err != nil { 57 | return "", fmt.Errorf("failed to create temp folder: %w", err) 58 | } 59 | 60 | dep.Logger().Info("Temp folder created: %s", tempPath) 61 | return tempPath, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/queue/metric.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import "sync/atomic" 4 | 5 | // Metric interface 6 | type Metric interface { 7 | IncBusyWorker() 8 | DecBusyWorker() 9 | BusyWorkers() uint64 10 | SuccessTasks() uint64 11 | FailureTasks() uint64 12 | SubmittedTasks() uint64 13 | IncSuccessTask() 14 | IncFailureTask() 15 | IncSubmittedTask() 16 | } 17 | 18 | var _ Metric = (*metric)(nil) 19 | 20 | type metric struct { 21 | busyWorkers uint64 22 | successTasks uint64 23 | failureTasks uint64 24 | submittedTasks uint64 25 | suspendingTasks uint64 26 | } 27 | 28 | // NewMetric for default metric structure 29 | func NewMetric() Metric { 30 | return &metric{} 31 | } 32 | 33 | func (m *metric) IncBusyWorker() { 34 | atomic.AddUint64(&m.busyWorkers, 1) 35 | } 36 | 37 | func (m *metric) DecBusyWorker() { 38 | atomic.AddUint64(&m.busyWorkers, ^uint64(0)) 39 | } 40 | 41 | func (m *metric) BusyWorkers() uint64 { 42 | return atomic.LoadUint64(&m.busyWorkers) 43 | } 44 | 45 | func (m *metric) IncSuccessTask() { 46 | atomic.AddUint64(&m.successTasks, 1) 47 | } 48 | 49 | func (m *metric) IncFailureTask() { 50 | atomic.AddUint64(&m.failureTasks, 1) 51 | } 52 | 53 | func (m *metric) IncSubmittedTask() { 54 | atomic.AddUint64(&m.submittedTasks, 1) 55 | } 56 | 57 | func (m *metric) SuccessTasks() uint64 { 58 | return atomic.LoadUint64(&m.successTasks) 59 | } 60 | 61 | func (m *metric) FailureTasks() uint64 { 62 | return atomic.LoadUint64(&m.failureTasks) 63 | } 64 | 65 | func (m *metric) SubmittedTasks() uint64 { 66 | return atomic.LoadUint64(&m.submittedTasks) 67 | } 68 | 69 | func (m *metric) SuspendingTasks() uint64 { 70 | return atomic.LoadUint64(&m.suspendingTasks) 71 | } 72 | 73 | func (m *metric) IncSuspendingTask() { 74 | atomic.AddUint64(&m.suspendingTasks, 1) 75 | } 76 | 77 | func (m *metric) DecSuspendingTask() { 78 | atomic.AddUint64(&m.suspendingTasks, ^uint64(0)) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/queue/registry.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import "sync" 4 | 5 | type ( 6 | // TaskRegistry is used in slave node to track in-memory stateful tasks. 7 | TaskRegistry interface { 8 | // NextID returns the next available Task ID. 9 | NextID() int 10 | // Get returns the Task by ID. 11 | Get(id int) (Task, bool) 12 | // Set sets the Task by ID. 13 | Set(id int, t Task) 14 | // Delete deletes the Task by ID. 15 | Delete(id int) 16 | } 17 | 18 | taskRegistry struct { 19 | tasks map[int]Task 20 | current int 21 | mu sync.Mutex 22 | } 23 | ) 24 | 25 | // NewTaskRegistry creates a new TaskRegistry. 26 | func NewTaskRegistry() TaskRegistry { 27 | return &taskRegistry{ 28 | tasks: make(map[int]Task), 29 | } 30 | } 31 | 32 | func (r *taskRegistry) NextID() int { 33 | r.mu.Lock() 34 | defer r.mu.Unlock() 35 | 36 | r.current++ 37 | return r.current 38 | } 39 | 40 | func (r *taskRegistry) Get(id int) (Task, bool) { 41 | r.mu.Lock() 42 | defer r.mu.Unlock() 43 | 44 | t, ok := r.tasks[id] 45 | return t, ok 46 | } 47 | 48 | func (r *taskRegistry) Set(id int, t Task) { 49 | r.mu.Lock() 50 | defer r.mu.Unlock() 51 | 52 | r.tasks[id] = t 53 | } 54 | 55 | func (r *taskRegistry) Delete(id int) { 56 | r.mu.Lock() 57 | defer r.mu.Unlock() 58 | 59 | delete(r.tasks, id) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/queue/thread.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import "sync" 4 | 5 | type routineGroup struct { 6 | waitGroup sync.WaitGroup 7 | } 8 | 9 | func newRoutineGroup() *routineGroup { 10 | return new(routineGroup) 11 | } 12 | 13 | func (g *routineGroup) Run(fn func()) { 14 | g.waitGroup.Add(1) 15 | 16 | go func() { 17 | defer g.waitGroup.Done() 18 | fn() 19 | }() 20 | } 21 | 22 | func (g *routineGroup) Wait() { 23 | g.waitGroup.Wait() 24 | } 25 | -------------------------------------------------------------------------------- /pkg/request/tpslimiter.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | "golang.org/x/time/rate" 6 | "sync" 7 | ) 8 | 9 | var globalTPSLimiter = NewTPSLimiter() 10 | 11 | type TPSLimiter interface { 12 | Limit(ctx context.Context, token string, tps float64, burst int) 13 | } 14 | 15 | func NewTPSLimiter() TPSLimiter { 16 | return &multipleBucketLimiter{ 17 | buckets: make(map[string]*rate.Limiter), 18 | } 19 | } 20 | 21 | // multipleBucketLimiter implements TPSLimiter with multiple bucket support. 22 | type multipleBucketLimiter struct { 23 | mu sync.Mutex 24 | buckets map[string]*rate.Limiter 25 | } 26 | 27 | // Limit finds the given bucket, if bucket not exist or limit is changed, 28 | // a new bucket will be generated. 29 | func (m *multipleBucketLimiter) Limit(ctx context.Context, token string, tps float64, burst int) { 30 | m.mu.Lock() 31 | bucket, ok := m.buckets[token] 32 | if !ok || float64(bucket.Limit()) != tps || bucket.Burst() != burst { 33 | bucket = rate.NewLimiter(rate.Limit(tps), burst) 34 | m.buckets[token] = bucket 35 | } 36 | m.mu.Unlock() 37 | 38 | bucket.Wait(ctx) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/request/tpslimiter_test.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestLimit(t *testing.T) { 11 | a := assert.New(t) 12 | l := NewTPSLimiter() 13 | finished := make(chan struct{}) 14 | go func() { 15 | l.Limit(context.Background(), "token", 1, 1) 16 | close(finished) 17 | }() 18 | select { 19 | case <-finished: 20 | case <-time.After(10 * time.Second): 21 | a.Fail("Limit should be finished instantly.") 22 | } 23 | 24 | finished = make(chan struct{}) 25 | go func() { 26 | l.Limit(context.Background(), "token", 1, 1) 27 | close(finished) 28 | }() 29 | select { 30 | case <-finished: 31 | case <-time.After(2 * time.Second): 32 | a.Fail("Limit should be finished in 1 second.") 33 | } 34 | 35 | finished = make(chan struct{}) 36 | go func() { 37 | l.Limit(context.Background(), "token", 10, 1) 38 | close(finished) 39 | }() 40 | select { 41 | case <-finished: 42 | case <-time.After(1 * time.Second): 43 | a.Fail("Limit should be finished instantly.") 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /pkg/request/utils.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "strconv" 7 | ) 8 | 9 | var contentLengthHeaders = []string{ 10 | "Content-Length", 11 | "X-Expected-Entity-Length", // DavFS on MacOS 12 | } 13 | 14 | // BlackHole 将客户端发来的数据放入黑洞 15 | func BlackHole(r io.Reader) { 16 | io.Copy(io.Discard, r) 17 | } 18 | 19 | // SniffContentLength tries to get the content length from the request. It also returns 20 | // a reader that will limit to the sniffed content length. 21 | func SniffContentLength(r *http.Request) (LimitReaderCloser, int64, error) { 22 | for _, header := range contentLengthHeaders { 23 | if length := r.Header.Get(header); length != "" { 24 | res, err := strconv.ParseInt(length, 10, 64) 25 | if err != nil { 26 | return nil, 0, err 27 | } 28 | 29 | return newLimitReaderCloser(r.Body, res), res, nil 30 | } 31 | } 32 | return newLimitReaderCloser(r.Body, 0), 0, nil 33 | } 34 | 35 | type LimitReaderCloser interface { 36 | io.Reader 37 | io.Closer 38 | Count() int64 39 | } 40 | 41 | type limitReaderCloser struct { 42 | io.Reader 43 | io.Closer 44 | read int64 45 | } 46 | 47 | func newLimitReaderCloser(r io.ReadCloser, limit int64) LimitReaderCloser { 48 | return &limitReaderCloser{ 49 | Reader: io.LimitReader(r, limit), 50 | Closer: r, 51 | } 52 | } 53 | 54 | func (l *limitReaderCloser) Read(p []byte) (n int, err error) { 55 | n, err = l.Reader.Read(p) 56 | l.read += int64(n) 57 | return n, err 58 | } 59 | 60 | func (l *limitReaderCloser) Count() int64 { 61 | return l.read 62 | } 63 | -------------------------------------------------------------------------------- /pkg/serializer/auth.go: -------------------------------------------------------------------------------- 1 | package serializer 2 | 3 | import "encoding/json" 4 | 5 | // RequestRawSign 待签名的HTTP请求 6 | type RequestRawSign struct { 7 | Path string 8 | Header string 9 | Body string 10 | } 11 | 12 | // NewRequestSignString 返回JSON格式的待签名字符串 13 | func NewRequestSignString(path, header, body string) string { 14 | req := RequestRawSign{ 15 | Path: path, 16 | Header: header, 17 | Body: body, 18 | } 19 | res, _ := json.Marshal(req) 20 | return string(res) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/serializer/response.go: -------------------------------------------------------------------------------- 1 | package serializer 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "encoding/gob" 8 | ) 9 | 10 | // Response 基础序列化器 11 | type Response struct { 12 | Code int `json:"code"` 13 | Data interface{} `json:"data,omitempty"` 14 | AggregatedError interface{} `json:"aggregated_error,omitempty"` 15 | Msg string `json:"msg"` 16 | Error string `json:"error,omitempty"` 17 | CorrelationID string `json:"correlation_id,omitempty"` 18 | } 19 | 20 | // NewResponseWithGobData 返回Data字段使用gob编码的Response 21 | func NewResponseWithGobData(c context.Context, data interface{}) Response { 22 | var w bytes.Buffer 23 | encoder := gob.NewEncoder(&w) 24 | if err := encoder.Encode(data); err != nil { 25 | return ErrWithDetails(c, CodeInternalSetting, "Failed to encode response content", err) 26 | } 27 | 28 | return Response{Data: w.Bytes()} 29 | } 30 | 31 | // GobDecode 将 Response 正文解码至目标指针 32 | func (r *Response) GobDecode(target interface{}) { 33 | src := r.Data.(string) 34 | raw := make([]byte, len(src)*len(src)/base64.StdEncoding.DecodedLen(len(src))) 35 | base64.StdEncoding.Decode(raw, []byte(src)) 36 | decoder := gob.NewDecoder(bytes.NewBuffer(raw)) 37 | decoder.Decode(target) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/serializer/setting.go: -------------------------------------------------------------------------------- 1 | package serializer 2 | 3 | // VolResponse VOL query response 4 | type VolResponse struct { 5 | Signature string `json:"signature"` 6 | Content string `json:"content"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/serializer/upload.go: -------------------------------------------------------------------------------- 1 | package serializer 2 | 3 | // GeneralUploadCallbackFailed 存储策略上传回调失败响应 4 | type GeneralUploadCallbackFailed struct { 5 | Error string `json:"error"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/sessionstore/sessionstore.go: -------------------------------------------------------------------------------- 1 | package sessionstore 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/pkg/cache" 5 | "github.com/gin-contrib/sessions" 6 | ) 7 | 8 | type Store interface { 9 | sessions.Store 10 | } 11 | 12 | func NewStore(driver cache.Driver, keyPairs ...[]byte) Store { 13 | return &store{newKvStore("cd_session_", driver, keyPairs...)} 14 | } 15 | 16 | type store struct { 17 | *kvStore 18 | } 19 | 20 | func (c *store) Options(options sessions.Options) { 21 | c.kvStore.Options = options.ToGorillaOptions() 22 | } 23 | -------------------------------------------------------------------------------- /pkg/thumb/music.go: -------------------------------------------------------------------------------- 1 | package thumb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/logging" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/setting" 9 | "github.com/cloudreve/Cloudreve/v4/pkg/util" 10 | "github.com/dhowden/tag" 11 | "github.com/gofrs/uuid" 12 | "os" 13 | "path/filepath" 14 | ) 15 | 16 | func NewMusicCoverGenerator(l logging.Logger, settings setting.Provider) *MusicCoverGenerator { 17 | return &MusicCoverGenerator{l: l, settings: settings} 18 | } 19 | 20 | type MusicCoverGenerator struct { 21 | l logging.Logger 22 | settings setting.Provider 23 | } 24 | 25 | func (v *MusicCoverGenerator) Generate(ctx context.Context, es entitysource.EntitySource, ext string, previous *Result) (*Result, error) { 26 | if !util.IsInExtensionListExt(v.settings.MusicCoverThumbExts(ctx), ext) { 27 | return nil, fmt.Errorf("unsupported music format: %w", ErrPassThrough) 28 | } 29 | 30 | if es.Entity().Size() > v.settings.MusicCoverThumbMaxSize(ctx) { 31 | return nil, fmt.Errorf("file is too big: %w", ErrPassThrough) 32 | } 33 | 34 | m, err := tag.ReadFrom(es) 35 | if err != nil { 36 | return nil, fmt.Errorf("faield to read audio tags from file: %w", err) 37 | } 38 | 39 | p := m.Picture() 40 | if p == nil || len(p.Data) == 0 { 41 | return nil, fmt.Errorf("no cover found in given file") 42 | } 43 | 44 | thumbExt := ".jpg" 45 | if p.Ext != "" { 46 | thumbExt = p.Ext 47 | } 48 | 49 | tempPath := filepath.Join( 50 | util.DataPath(v.settings.TempPath(ctx)), 51 | thumbTempFolder, 52 | fmt.Sprintf("thumb_%s.%s", uuid.Must(uuid.NewV4()).String(), thumbExt), 53 | ) 54 | 55 | thumbFile, err := util.CreatNestedFile(tempPath) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to create temp file: %w", err) 58 | } 59 | 60 | defer thumbFile.Close() 61 | 62 | if _, err := thumbFile.Write(p.Data); err != nil { 63 | return &Result{Path: tempPath}, fmt.Errorf("failed to write cover to file: %w", err) 64 | } 65 | 66 | return &Result{ 67 | Path: tempPath, 68 | Continue: true, 69 | Cleanup: []func(){func() { _ = os.Remove(tempPath) }}, 70 | }, nil 71 | } 72 | 73 | func (v *MusicCoverGenerator) Priority() int { 74 | return 50 75 | } 76 | 77 | func (v *MusicCoverGenerator) Enabled(ctx context.Context) bool { 78 | return v.settings.MusicCoverThumbGeneratorEnabled(ctx) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/thumb/tester.go: -------------------------------------------------------------------------------- 1 | package thumb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | ErrUnknownGenerator = errors.New("unknown generator type") 14 | ErrUnknownOutput = errors.New("unknown output from generator") 15 | ) 16 | 17 | // TestGenerator tests thumb generator by getting lib version 18 | func TestGenerator(ctx context.Context, name, executable string) (string, error) { 19 | switch name { 20 | case "vips": 21 | return testVipsGenerator(ctx, executable) 22 | case "ffmpeg": 23 | return testFfmpegGenerator(ctx, executable) 24 | case "libreOffice": 25 | return testLibreOfficeGenerator(ctx, executable) 26 | case "ffprobe": 27 | return testFFProbeGenerator(ctx, executable) 28 | default: 29 | return "", ErrUnknownGenerator 30 | } 31 | } 32 | 33 | func testFFProbeGenerator(ctx context.Context, executable string) (string, error) { 34 | cmd := exec.CommandContext(ctx, executable, "-version") 35 | var output bytes.Buffer 36 | cmd.Stdout = &output 37 | if err := cmd.Run(); err != nil { 38 | return "", fmt.Errorf("failed to invoke ffmpeg executable: %w", err) 39 | } 40 | 41 | if !strings.Contains(output.String(), "ffprobe") { 42 | return "", ErrUnknownOutput 43 | } 44 | 45 | return output.String(), nil 46 | } 47 | 48 | func testVipsGenerator(ctx context.Context, executable string) (string, error) { 49 | cmd := exec.CommandContext(ctx, executable, "--version") 50 | var output bytes.Buffer 51 | cmd.Stdout = &output 52 | if err := cmd.Run(); err != nil { 53 | return "", fmt.Errorf("failed to invoke vips executable: %w", err) 54 | } 55 | 56 | if !strings.Contains(output.String(), "vips") { 57 | return "", ErrUnknownOutput 58 | } 59 | 60 | return output.String(), nil 61 | } 62 | 63 | func testFfmpegGenerator(ctx context.Context, executable string) (string, error) { 64 | cmd := exec.CommandContext(ctx, executable, "-version") 65 | var output bytes.Buffer 66 | cmd.Stdout = &output 67 | if err := cmd.Run(); err != nil { 68 | return "", fmt.Errorf("failed to invoke ffmpeg executable: %w", err) 69 | } 70 | 71 | if !strings.Contains(output.String(), "ffmpeg") { 72 | return "", ErrUnknownOutput 73 | } 74 | 75 | return output.String(), nil 76 | } 77 | 78 | func testLibreOfficeGenerator(ctx context.Context, executable string) (string, error) { 79 | cmd := exec.CommandContext(ctx, executable, "--version") 80 | var output bytes.Buffer 81 | cmd.Stdout = &output 82 | if err := cmd.Run(); err != nil { 83 | return "", fmt.Errorf("failed to invoke libreoffice executable: %w", err) 84 | } 85 | 86 | if !strings.Contains(output.String(), "LibreOffice") { 87 | return "", ErrUnknownOutput 88 | } 89 | 90 | return output.String(), nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/util/common_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestRandStringRunes(t *testing.T) { 9 | asserts := assert.New(t) 10 | 11 | // 0 长度字符 12 | randStr := RandStringRunes(0) 13 | asserts.Len(randStr, 0) 14 | 15 | // 16 长度字符 16 | randStr = RandStringRunes(16) 17 | asserts.Len(randStr, 16) 18 | 19 | // 32 长度字符 20 | randStr = RandStringRunes(32) 21 | asserts.Len(randStr, 32) 22 | 23 | //相同长度字符 24 | sameLenStr1 := RandStringRunes(32) 25 | sameLenStr2 := RandStringRunes(32) 26 | asserts.NotEqual(sameLenStr1, sameLenStr2) 27 | } 28 | 29 | func TestContainsUint(t *testing.T) { 30 | asserts := assert.New(t) 31 | asserts.True(ContainsUint([]uint{0, 2, 3, 65, 4}, 65)) 32 | asserts.True(ContainsUint([]uint{65}, 65)) 33 | asserts.False(ContainsUint([]uint{65}, 6)) 34 | } 35 | 36 | func TestContainsString(t *testing.T) { 37 | asserts := assert.New(t) 38 | asserts.True(ContainsString([]string{"", "1"}, "")) 39 | asserts.True(ContainsString([]string{"", "1"}, "1")) 40 | asserts.False(ContainsString([]string{"", "1"}, " ")) 41 | } 42 | 43 | func TestReplace(t *testing.T) { 44 | asserts := assert.New(t) 45 | 46 | asserts.Equal("origin", Replace(map[string]string{ 47 | "123": "321", 48 | }, "origin")) 49 | 50 | asserts.Equal("321origin321", Replace(map[string]string{ 51 | "123": "321", 52 | }, "123origin123")) 53 | asserts.Equal("321new321", Replace(map[string]string{ 54 | "123": "321", 55 | "origin": "new", 56 | }, "123origin123")) 57 | } 58 | 59 | func TestBuildRegexp(t *testing.T) { 60 | asserts := assert.New(t) 61 | 62 | asserts.Equal("^/dir/", BuildRegexp([]string{"/dir"}, "^", "/", "|")) 63 | asserts.Equal("^/dir/|^/dir/di\\*r/", BuildRegexp([]string{"/dir", "/dir/di*r"}, "^", "/", "|")) 64 | } 65 | 66 | func TestBuildConcat(t *testing.T) { 67 | asserts := assert.New(t) 68 | asserts.Equal("CONCAT(1,2)", BuildConcat("1", "2", "mysql")) 69 | asserts.Equal("1||2", BuildConcat("1", "2", "sqlite")) 70 | } 71 | 72 | func TestSliceDifference(t *testing.T) { 73 | asserts := assert.New(t) 74 | 75 | { 76 | s1 := []string{"1", "2", "3", "4"} 77 | s2 := []string{"2", "4"} 78 | asserts.Equal([]string{"1", "3"}, SliceDifference(s1, s2)) 79 | } 80 | 81 | { 82 | s2 := []string{"1", "2", "3", "4"} 83 | s1 := []string{"2", "4"} 84 | asserts.Equal([]string{}, SliceDifference(s1, s2)) 85 | } 86 | 87 | { 88 | s1 := []string{"1", "2", "3", "4"} 89 | s2 := []string{"1", "2", "3", "4"} 90 | asserts.Equal([]string{}, SliceDifference(s1, s2)) 91 | } 92 | 93 | { 94 | s1 := []string{"1", "2", "3", "4"} 95 | s2 := []string{} 96 | asserts.Equal([]string{"1", "2", "3", "4"}, SliceDifference(s1, s2)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/util/io.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // Exists reports whether the named file or directory exists. 10 | func Exists(name string) bool { 11 | if _, err := os.Stat(name); err != nil { 12 | if os.IsNotExist(err) { 13 | return false 14 | } 15 | } 16 | return true 17 | } 18 | 19 | // CreatNestedFile 给定path创建文件,如果目录不存在就递归创建 20 | func CreatNestedFile(path string) (*os.File, error) { 21 | basePath := filepath.Dir(path) 22 | if !Exists(basePath) { 23 | err := os.MkdirAll(basePath, 0700) 24 | if err != nil { 25 | return nil, err 26 | } 27 | } 28 | 29 | return os.Create(path) 30 | } 31 | 32 | // CreatNestedFolder creates a folder with the given path, if the directory does not exist, 33 | // it will be created recursively. 34 | func CreatNestedFolder(path string) error { 35 | if !Exists(path) { 36 | err := os.MkdirAll(path, 0700) 37 | if err != nil { 38 | return err 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // IsEmpty 返回给定目录是否为空目录 46 | func IsEmpty(name string) (bool, error) { 47 | f, err := os.Open(name) 48 | if err != nil { 49 | return false, err 50 | } 51 | defer f.Close() 52 | 53 | _, err = f.Readdirnames(1) // Or f.Readdir(1) 54 | if err == io.EOF { 55 | return true, nil 56 | } 57 | return false, err // Either not empty or error, suits both cases 58 | } 59 | 60 | type CallbackReader struct { 61 | reader io.Reader 62 | callback func(int64) 63 | } 64 | 65 | func NewCallbackReader(reader io.Reader, callback func(int64)) *CallbackReader { 66 | return &CallbackReader{ 67 | reader: reader, 68 | callback: callback, 69 | } 70 | } 71 | 72 | func (r *CallbackReader) Read(p []byte) (n int, err error) { 73 | n, err = r.reader.Read(p) 74 | r.callback(int64(n)) 75 | return 76 | } 77 | -------------------------------------------------------------------------------- /pkg/util/io_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestExists(t *testing.T) { 9 | asserts := assert.New(t) 10 | asserts.True(Exists("io_test.go")) 11 | asserts.False(Exists("io_test.js")) 12 | } 13 | 14 | func TestCreatNestedFile(t *testing.T) { 15 | asserts := assert.New(t) 16 | 17 | // 父目录不存在 18 | { 19 | file, err := CreatNestedFile("test/nest.txt") 20 | asserts.NoError(err) 21 | asserts.NoError(file.Close()) 22 | asserts.FileExists("test/nest.txt") 23 | } 24 | 25 | // 父目录存在 26 | { 27 | file, err := CreatNestedFile("test/direct.txt") 28 | asserts.NoError(err) 29 | asserts.NoError(file.Close()) 30 | asserts.FileExists("test/direct.txt") 31 | } 32 | } 33 | 34 | func TestIsEmpty(t *testing.T) { 35 | asserts := assert.New(t) 36 | 37 | asserts.False(IsEmpty("")) 38 | asserts.False(IsEmpty("not_exist")) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/util/logger_test.go: -------------------------------------------------------------------------------- 1 | // +build !race 2 | 3 | package util 4 | 5 | import ( 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestBuildLogger(t *testing.T) { 11 | asserts := assert.New(t) 12 | asserts.NotPanics(func() { 13 | BuildLogger("error") 14 | }) 15 | asserts.NotPanics(func() { 16 | BuildLogger("warning") 17 | }) 18 | asserts.NotPanics(func() { 19 | BuildLogger("info") 20 | }) 21 | asserts.NotPanics(func() { 22 | BuildLogger("?") 23 | }) 24 | asserts.NotPanics(func() { 25 | BuildLogger("debug") 26 | }) 27 | } 28 | 29 | func TestLog(t *testing.T) { 30 | asserts := assert.New(t) 31 | asserts.NotNil(Log()) 32 | GloablLogger = nil 33 | asserts.NotNil(Log()) 34 | } 35 | 36 | func TestLogger_Debug(t *testing.T) { 37 | asserts := assert.New(t) 38 | l := Logger{ 39 | level: LevelDebug, 40 | } 41 | asserts.NotPanics(func() { 42 | l.Debug("123") 43 | }) 44 | l.level = LevelError 45 | asserts.NotPanics(func() { 46 | l.Debug("123") 47 | }) 48 | } 49 | 50 | func TestLogger_Info(t *testing.T) { 51 | asserts := assert.New(t) 52 | l := Logger{ 53 | level: LevelDebug, 54 | } 55 | asserts.NotPanics(func() { 56 | l.Info("123") 57 | }) 58 | l.level = LevelError 59 | asserts.NotPanics(func() { 60 | l.Info("123") 61 | }) 62 | } 63 | func TestLogger_Warning(t *testing.T) { 64 | asserts := assert.New(t) 65 | l := Logger{ 66 | level: LevelDebug, 67 | } 68 | asserts.NotPanics(func() { 69 | l.Warning("123") 70 | }) 71 | l.level = LevelError 72 | asserts.NotPanics(func() { 73 | l.Warning("123") 74 | }) 75 | } 76 | 77 | func TestLogger_Error(t *testing.T) { 78 | asserts := assert.New(t) 79 | l := Logger{ 80 | level: LevelDebug, 81 | } 82 | asserts.NotPanics(func() { 83 | l.Error("123") 84 | }) 85 | l.level = -1 86 | asserts.NotPanics(func() { 87 | l.Error("123") 88 | }) 89 | } 90 | 91 | func TestLogger_Panic(t *testing.T) { 92 | asserts := assert.New(t) 93 | l := Logger{ 94 | level: LevelDebug, 95 | } 96 | asserts.Panics(func() { 97 | l.Panic("123") 98 | }) 99 | l.level = -1 100 | asserts.NotPanics(func() { 101 | l.Error("123") 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/util/path.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | DataFolder = "data" 13 | ) 14 | 15 | var UseWorkingDir = false 16 | 17 | // DotPathToStandardPath 将","分割的路径转换为标准路径 18 | func DotPathToStandardPath(path string) string { 19 | return "/" + strings.Replace(path, ",", "/", -1) 20 | } 21 | 22 | // FillSlash 给路径补全`/` 23 | func FillSlash(path string) string { 24 | if path == "/" { 25 | return path 26 | } 27 | return path + "/" 28 | } 29 | 30 | // RemoveSlash 移除路径最后的`/` 31 | func RemoveSlash(path string) string { 32 | if len(path) > 1 { 33 | return strings.TrimSuffix(path, "/") 34 | } 35 | return path 36 | } 37 | 38 | // SplitPath 分割路径为列表 39 | func SplitPath(path string) []string { 40 | if len(path) == 0 || path[0] != '/' { 41 | return []string{} 42 | } 43 | 44 | if path == "/" { 45 | return []string{"/"} 46 | } 47 | 48 | pathSplit := strings.Split(path, "/") 49 | pathSplit[0] = "/" 50 | return pathSplit 51 | } 52 | 53 | // FormSlash 将path中的反斜杠'\'替换为'/' 54 | func FormSlash(old string) string { 55 | return path.Clean(strings.ReplaceAll(old, "\\", "/")) 56 | } 57 | 58 | // RelativePath 获取相对可执行文件的路径 59 | func RelativePath(name string) string { 60 | if UseWorkingDir { 61 | return name 62 | } 63 | 64 | if filepath.IsAbs(name) { 65 | return name 66 | } 67 | e, _ := os.Executable() 68 | return filepath.Join(filepath.Dir(e), name) 69 | } 70 | 71 | // DataPath relative path for store persist data file 72 | func DataPath(child string) string { 73 | dataPath := RelativePath(DataFolder) 74 | if !Exists(dataPath) { 75 | os.MkdirAll(dataPath, 0700) 76 | } 77 | 78 | if filepath.IsAbs(child) { 79 | return child 80 | } 81 | 82 | return filepath.Join(dataPath, child) 83 | } 84 | 85 | // MkdirIfNotExist create directory if not exist 86 | func MkdirIfNotExist(ctx context.Context, p string) { 87 | if !Exists(p) { 88 | os.MkdirAll(p, 0700) 89 | } 90 | } 91 | 92 | // SlashClean is equivalent to but slightly more efficient than 93 | // path.Clean("/" + name). 94 | func SlashClean(name string) string { 95 | if name == "" || name[0] != '/' { 96 | name = "/" + name 97 | } 98 | return path.Clean(name) 99 | } 100 | 101 | // Ext returns the file name extension used by path, without the dot. 102 | func Ext(name string) string { 103 | ext := strings.ToLower(filepath.Ext(name)) 104 | if len(ext) > 0 { 105 | ext = ext[1:] 106 | } 107 | return ext 108 | } 109 | -------------------------------------------------------------------------------- /pkg/util/path_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestDotPathToStandardPath(t *testing.T) { 9 | asserts := assert.New(t) 10 | 11 | asserts.Equal("/", DotPathToStandardPath("")) 12 | asserts.Equal("/目录", DotPathToStandardPath("目录")) 13 | asserts.Equal("/目录/目录2", DotPathToStandardPath("目录,目录2")) 14 | } 15 | 16 | func TestFillSlash(t *testing.T) { 17 | asserts := assert.New(t) 18 | asserts.Equal("/", FillSlash("/")) 19 | asserts.Equal("/", FillSlash("")) 20 | asserts.Equal("/123/", FillSlash("/123")) 21 | } 22 | 23 | func TestRemoveSlash(t *testing.T) { 24 | asserts := assert.New(t) 25 | asserts.Equal("/", RemoveSlash("/")) 26 | asserts.Equal("/123/1236", RemoveSlash("/123/1236")) 27 | asserts.Equal("/123/1236", RemoveSlash("/123/1236/")) 28 | } 29 | 30 | func TestSplitPath(t *testing.T) { 31 | asserts := assert.New(t) 32 | asserts.Equal([]string{}, SplitPath("")) 33 | asserts.Equal([]string{}, SplitPath("1")) 34 | asserts.Equal([]string{"/"}, SplitPath("/")) 35 | asserts.Equal([]string{"/", "123", "321"}, SplitPath("/123/321")) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/util/session.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/gin-contrib/sessions" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | // SetSession 设置session 9 | func SetSession(c *gin.Context, list map[string]interface{}) { 10 | s := sessions.Default(c) 11 | for key, value := range list { 12 | s.Set(key, value) 13 | } 14 | 15 | err := s.Save() 16 | if err != nil { 17 | Log().Warning("无法设置 Session 值:%s", err) 18 | } 19 | } 20 | 21 | // GetSession 获取session 22 | func GetSession(c *gin.Context, key any) interface{} { 23 | s := sessions.Default(c) 24 | return s.Get(key) 25 | } 26 | 27 | // DeleteSession 删除session 28 | func DeleteSession(c *gin.Context, key any) { 29 | s := sessions.Default(c) 30 | s.Delete(key) 31 | s.Save() 32 | } 33 | 34 | // ClearSession 清空session 35 | func ClearSession(c *gin.Context) { 36 | s := sessions.Default(c) 37 | s.Clear() 38 | s.Save() 39 | } 40 | -------------------------------------------------------------------------------- /pkg/webdav/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package webdav 6 | 7 | import ( 8 | "path" 9 | ) 10 | 11 | // slashClean is equivalent to but slightly more efficient than 12 | // path.Clean("/" + name). 13 | func slashClean(name string) string { 14 | if name == "" || name[0] != '/' { 15 | name = "/" + name 16 | } 17 | return path.Clean(name) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/webdav/internal/xml/README: -------------------------------------------------------------------------------- 1 | This is a fork of the encoding/xml package at ca1d6c4, the last commit before 2 | https://go.googlesource.com/go/+/c0d6d33 "encoding/xml: restore Go 1.4 name 3 | space behavior" made late in the lead-up to the Go 1.5 release. 4 | 5 | The list of encoding/xml changes is at 6 | https://go.googlesource.com/go/+log/master/src/encoding/xml 7 | 8 | This fork is temporary, and I (nigeltao) expect to revert it after Go 1.6 is 9 | released. 10 | 11 | See http://golang.org/issue/11841 12 | -------------------------------------------------------------------------------- /pkg/wopi/discovery.go: -------------------------------------------------------------------------------- 1 | package wopi 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/setting" 7 | "github.com/gofrs/uuid" 8 | "github.com/samber/lo" 9 | ) 10 | 11 | type ActonType string 12 | 13 | var ( 14 | ActionPreview = ActonType("embedview") 15 | ActionPreviewFallback = ActonType("view") 16 | ActionEdit = ActonType("edit") 17 | ) 18 | 19 | func DiscoveryXmlToViewerGroup(xmlStr string) (*setting.ViewerGroup, error) { 20 | var discovery WopiDiscovery 21 | if err := xml.Unmarshal([]byte(xmlStr), &discovery); err != nil { 22 | return nil, fmt.Errorf("failed to parse WOPI discovery XML: %w", err) 23 | } 24 | 25 | group := &setting.ViewerGroup{ 26 | Viewers: make([]setting.Viewer, 0, len(discovery.NetZone.App)), 27 | } 28 | 29 | for _, app := range discovery.NetZone.App { 30 | viewer := setting.Viewer{ 31 | ID: uuid.Must(uuid.NewV4()).String(), 32 | DisplayName: app.Name, 33 | Type: setting.ViewerTypeWopi, 34 | Icon: app.FavIconUrl, 35 | WopiActions: make(map[string]map[setting.ViewerAction]string), 36 | } 37 | 38 | for _, action := range app.Action { 39 | if action.Ext == "" { 40 | continue 41 | } 42 | 43 | if _, ok := viewer.WopiActions[action.Ext]; !ok { 44 | viewer.WopiActions[action.Ext] = make(map[setting.ViewerAction]string) 45 | } 46 | 47 | if action.Name == string(ActionPreview) { 48 | viewer.WopiActions[action.Ext][setting.ViewerActionView] = action.Urlsrc 49 | } else if action.Name == string(ActionPreviewFallback) { 50 | viewer.WopiActions[action.Ext][setting.ViewerActionView] = action.Urlsrc 51 | } else if action.Name == string(ActionEdit) { 52 | viewer.WopiActions[action.Ext][setting.ViewerActionEdit] = action.Urlsrc 53 | } else if len(viewer.WopiActions[action.Ext]) == 0 { 54 | delete(viewer.WopiActions, action.Ext) 55 | } 56 | } 57 | 58 | viewer.Exts = lo.MapToSlice(viewer.WopiActions, func(key string, value map[setting.ViewerAction]string) string { 59 | return key 60 | }) 61 | 62 | if len(viewer.WopiActions) > 0 { 63 | group.Viewers = append(group.Viewers, viewer) 64 | } 65 | } 66 | 67 | return group, nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/wopi/types.go: -------------------------------------------------------------------------------- 1 | package wopi 2 | 3 | import ( 4 | "encoding/gob" 5 | "encoding/xml" 6 | "net/url" 7 | ) 8 | 9 | // Response content from discovery endpoint. 10 | type WopiDiscovery struct { 11 | XMLName xml.Name `xml:"wopi-discovery"` 12 | Text string `xml:",chardata"` 13 | NetZone struct { 14 | Text string `xml:",chardata"` 15 | Name string `xml:"name,attr"` 16 | App []struct { 17 | Text string `xml:",chardata"` 18 | Name string `xml:"name,attr"` 19 | FavIconUrl string `xml:"favIconUrl,attr"` 20 | BootstrapperUrl string `xml:"bootstrapperUrl,attr"` 21 | AppBootstrapperUrl string `xml:"appBootstrapperUrl,attr"` 22 | ApplicationBaseUrl string `xml:"applicationBaseUrl,attr"` 23 | StaticResourceOrigin string `xml:"staticResourceOrigin,attr"` 24 | CheckLicense string `xml:"checkLicense,attr"` 25 | Action []Action `xml:"action"` 26 | } `xml:"app"` 27 | } `xml:"net-zone"` 28 | ProofKey struct { 29 | Text string `xml:",chardata"` 30 | Oldvalue string `xml:"oldvalue,attr"` 31 | Oldmodulus string `xml:"oldmodulus,attr"` 32 | Oldexponent string `xml:"oldexponent,attr"` 33 | Value string `xml:"value,attr"` 34 | Modulus string `xml:"modulus,attr"` 35 | Exponent string `xml:"exponent,attr"` 36 | } `xml:"proof-key"` 37 | } 38 | 39 | type Action struct { 40 | Text string `xml:",chardata"` 41 | Name string `xml:"name,attr"` 42 | Ext string `xml:"ext,attr"` 43 | Default string `xml:"default,attr"` 44 | Urlsrc string `xml:"urlsrc,attr"` 45 | Requires string `xml:"requires,attr"` 46 | Targetext string `xml:"targetext,attr"` 47 | Progid string `xml:"progid,attr"` 48 | UseParent string `xml:"useParent,attr"` 49 | Newprogid string `xml:"newprogid,attr"` 50 | Newext string `xml:"newext,attr"` 51 | } 52 | 53 | type Session struct { 54 | AccessToken string 55 | AccessTokenTTL int64 56 | ActionURL *url.URL 57 | } 58 | 59 | type SessionCache struct { 60 | AccessToken string 61 | FileID uint 62 | UserID uint 63 | Action ActonType 64 | } 65 | 66 | const ( 67 | WopiSessionCtx = "wopi_session" 68 | ) 69 | 70 | func init() { 71 | gob.Register(WopiDiscovery{}) 72 | gob.Register(Action{}) 73 | gob.Register(SessionCache{}) 74 | } 75 | -------------------------------------------------------------------------------- /routers/controllers/directory.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 6 | "github.com/cloudreve/Cloudreve/v4/service/explorer" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // ListDirectory 列出目录下内容 11 | func ListDirectory(c *gin.Context) { 12 | service := ParametersFromContext[*explorer.ListFileService](c, explorer.ListFileParameterCtx{}) 13 | resp, err := service.List(c) 14 | if err != nil { 15 | if errors.Is(err, explorer.ErrSSETakeOver) { 16 | return 17 | } 18 | 19 | c.JSON(200, serializer.Err(c, err)) 20 | c.Abort() 21 | return 22 | } 23 | 24 | c.JSON(200, serializer.Response{ 25 | Data: resp, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /routers/controllers/share.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/pkg/hashid" 5 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 6 | "github.com/cloudreve/Cloudreve/v4/service/share" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | // CreateShare 创建分享 12 | func CreateShare(c *gin.Context) { 13 | service := ParametersFromContext[*share.ShareCreateService](c, share.ShareCreateParamCtx{}) 14 | uri, err := service.Upsert(c, 0) 15 | if err != nil { 16 | c.JSON(200, serializer.Err(c, err)) 17 | return 18 | } 19 | 20 | c.JSON(200, serializer.Response{Data: uri}) 21 | } 22 | 23 | // EditShare 编辑分享 24 | func EditShare(c *gin.Context) { 25 | service := ParametersFromContext[*share.ShareCreateService](c, share.ShareCreateParamCtx{}) 26 | uri, err := service.Upsert(c, hashid.FromContext(c)) 27 | if err != nil { 28 | c.JSON(200, serializer.Err(c, err)) 29 | return 30 | } 31 | 32 | c.JSON(200, serializer.Response{Data: uri}) 33 | } 34 | 35 | // GetShare 查看分享 36 | func GetShare(c *gin.Context) { 37 | service := ParametersFromContext[*share.ShareInfoService](c, share.ShareInfoParamCtx{}) 38 | info, err := service.Get(c) 39 | if err != nil { 40 | c.JSON(200, serializer.Err(c, err)) 41 | return 42 | } 43 | 44 | c.JSON(200, serializer.Response{Data: info}) 45 | } 46 | 47 | // ListShare 列出分享 48 | func ListShare(c *gin.Context) { 49 | service := ParametersFromContext[*share.ListShareService](c, share.ListShareParamCtx{}) 50 | resp, err := service.List(c) 51 | if err != nil { 52 | c.JSON(200, serializer.Err(c, err)) 53 | c.Abort() 54 | return 55 | } 56 | 57 | if resp != nil { 58 | c.JSON(200, serializer.Response{ 59 | Data: resp, 60 | }) 61 | } 62 | } 63 | 64 | // DeleteShare 删除分享 65 | func DeleteShare(c *gin.Context) { 66 | err := share.DeleteShare(c, hashid.FromContext(c)) 67 | if err != nil { 68 | c.JSON(200, serializer.Err(c, err)) 69 | return 70 | } 71 | 72 | c.JSON(200, serializer.Response{}) 73 | } 74 | 75 | func ShareRedirect(c *gin.Context) { 76 | service := ParametersFromContext[*share.ShortLinkRedirectService](c, share.ShortLinkRedirectParamCtx{}) 77 | c.Redirect(http.StatusFound, service.RedirectTo(c)) 78 | } 79 | -------------------------------------------------------------------------------- /routers/controllers/site.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/application/constants" 5 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 7 | "github.com/cloudreve/Cloudreve/v4/service/basic" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // SiteConfig 获取站点全局配置 12 | func SiteConfig(c *gin.Context) { 13 | service := ParametersFromContext[*basic.GetSettingService](c, basic.GetSettingParamCtx{}) 14 | 15 | resp, err := service.GetSiteConfig(c) 16 | if err != nil { 17 | c.JSON(200, serializer.Err(c, err)) 18 | c.Abort() 19 | return 20 | } 21 | 22 | c.JSON(200, serializer.Response{ 23 | Data: resp, 24 | }) 25 | } 26 | 27 | // Ping 状态检查页面 28 | func Ping(c *gin.Context) { 29 | version := constants.BackendVersion 30 | if constants.IsProBool { 31 | version += "-pro" 32 | } 33 | 34 | c.JSON(200, serializer.Response{ 35 | Code: 0, 36 | Data: version, 37 | }) 38 | } 39 | 40 | // Captcha 获取验证码 41 | func Captcha(c *gin.Context) { 42 | c.JSON(200, serializer.Response{ 43 | Code: 0, 44 | Data: basic.GetCaptchaImage(c), 45 | }) 46 | } 47 | 48 | // Manifest 获取manifest.json 49 | func Manifest(c *gin.Context) { 50 | settingClient := dependency.FromContext(c).SettingProvider() 51 | siteOpts := settingClient.SiteBasic(c) 52 | pwaOpts := settingClient.PWA(c) 53 | c.Header("Cache-Control", "public, no-cache") 54 | c.JSON(200, map[string]interface{}{ 55 | "short_name": siteOpts.Name, 56 | "name": siteOpts.Name, 57 | "icons": []map[string]string{ 58 | { 59 | "src": pwaOpts.SmallIcon, 60 | "sizes": "64x64 32x32 24x24 16x16", 61 | "type": "image/x-icon", 62 | }, 63 | { 64 | "src": pwaOpts.MediumIcon, 65 | "type": "image/png", 66 | "sizes": "192x192", 67 | }, 68 | { 69 | "src": pwaOpts.LargeIcon, 70 | "type": "image/png", 71 | "sizes": "512x512", 72 | }, 73 | }, 74 | "start_url": ".", 75 | "display": pwaOpts.Display, 76 | "theme_color": pwaOpts.ThemeColor, 77 | "background_color": pwaOpts.BackgroundColor, 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /routers/controllers/wopi.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/cloudreve/Cloudreve/v4/pkg/wopi" 7 | "github.com/cloudreve/Cloudreve/v4/service/explorer" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // CheckFileInfo Get file info 12 | func CheckFileInfo(c *gin.Context) { 13 | var service explorer.WopiService 14 | res, err := service.FileInfo(c) 15 | if err != nil { 16 | c.Status(http.StatusInternalServerError) 17 | c.Header(wopi.ServerErrorHeader, err.Error()) 18 | return 19 | } 20 | 21 | c.JSON(200, res) 22 | } 23 | 24 | // GetFile Get file content 25 | func GetFile(c *gin.Context) { 26 | var service explorer.WopiService 27 | err := service.GetFile(c) 28 | if err != nil { 29 | c.Status(http.StatusInternalServerError) 30 | c.Header(wopi.ServerErrorHeader, err.Error()) 31 | return 32 | } 33 | } 34 | 35 | // PutFile Puts file content 36 | func PutFile(c *gin.Context) { 37 | service := &explorer.WopiService{} 38 | err := service.PutContent(c, false) 39 | if err != nil { 40 | c.Status(http.StatusInternalServerError) 41 | c.Header(wopi.ServerErrorHeader, err.Error()) 42 | } 43 | } 44 | 45 | // ModifyFile Modify file properties 46 | func ModifyFile(c *gin.Context) { 47 | action := c.GetHeader(wopi.OverwriteHeader) 48 | var ( 49 | service explorer.WopiService 50 | err error 51 | ) 52 | 53 | switch action { 54 | case wopi.MethodLock: 55 | err = service.Lock(c) 56 | if err == nil { 57 | return 58 | } 59 | case wopi.MethodRefreshLock: 60 | err = service.RefreshLock(c) 61 | if err == nil { 62 | return 63 | } 64 | case wopi.MethodUnlock: 65 | err = service.Unlock(c) 66 | if err == nil { 67 | return 68 | } 69 | case wopi.MethodPutRelative: 70 | err = service.PutContent(c, true) 71 | if err == nil { 72 | return 73 | } 74 | default: 75 | c.Status(http.StatusNotImplemented) 76 | return 77 | } 78 | 79 | c.Status(http.StatusInternalServerError) 80 | c.Header(wopi.ServerErrorHeader, err.Error()) 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /routers/controllers/workflow.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/pkg/hashid" 5 | "github.com/cloudreve/Cloudreve/v4/pkg/queue" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 7 | "github.com/cloudreve/Cloudreve/v4/service/explorer" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func ListTasks(c *gin.Context) { 12 | service := ParametersFromContext[*explorer.ListTaskService](c, explorer.ListTaskParamCtx{}) 13 | resp, err := service.ListTasks(c) 14 | if err != nil { 15 | c.JSON(200, serializer.Err(c, err)) 16 | c.Abort() 17 | return 18 | } 19 | 20 | if resp != nil { 21 | c.JSON(200, serializer.Response{ 22 | Data: resp, 23 | }) 24 | } 25 | } 26 | 27 | func GetTaskPhaseProgress(c *gin.Context) { 28 | taskId := hashid.FromContext(c) 29 | resp, err := explorer.TaskPhaseProgress(c, taskId) 30 | if err != nil { 31 | c.JSON(200, serializer.Err(c, err)) 32 | c.Abort() 33 | return 34 | } 35 | 36 | if resp != nil { 37 | c.JSON(200, serializer.Response{ 38 | Data: resp, 39 | }) 40 | } else { 41 | c.JSON(200, serializer.Response{Data: queue.Progresses{}}) 42 | } 43 | } 44 | 45 | func SetDownloadTaskTarget(c *gin.Context) { 46 | taskId := hashid.FromContext(c) 47 | service := ParametersFromContext[*explorer.SetDownloadFilesService](c, explorer.SetDownloadFilesParamCtx{}) 48 | err := service.SetDownloadFiles(c, taskId) 49 | if err != nil { 50 | c.JSON(200, serializer.Err(c, err)) 51 | c.Abort() 52 | return 53 | } 54 | 55 | c.JSON(200, serializer.Response{}) 56 | } 57 | 58 | func CancelDownloadTask(c *gin.Context) { 59 | taskId := hashid.FromContext(c) 60 | err := explorer.CancelDownloadTask(c, taskId) 61 | if err != nil { 62 | c.JSON(200, serializer.Err(c, err)) 63 | c.Abort() 64 | return 65 | } 66 | 67 | c.JSON(200, serializer.Response{}) 68 | } 69 | -------------------------------------------------------------------------------- /service/admin/list.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 5 | ) 6 | 7 | // AdminListService 仪表盘列条目服务 8 | type ( 9 | AdminListService struct { 10 | Page int `json:"page" binding:"min=1"` 11 | PageSize int `json:"page_size" binding:"min=1,required"` 12 | OrderBy string `json:"order_by"` 13 | OrderDirection string `json:"order_direction"` 14 | Conditions map[string]string `json:"conditions"` 15 | Searches map[string]string `json:"searches"` 16 | } 17 | AdminListServiceParamsCtx struct{} 18 | ) 19 | 20 | // GroupList 获取用户组列表 21 | func (service *NoParamService) GroupList() serializer.Response { 22 | //var res []model.Group 23 | //model.DB.Model(&model.Group{}).Find(&res) 24 | //return serializer.Response{Data: res} 25 | return serializer.Response{} 26 | } 27 | -------------------------------------------------------------------------------- /service/explorer/metadata.go: -------------------------------------------------------------------------------- 1 | package explorer 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 5 | "github.com/cloudreve/Cloudreve/v4/inventory" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type ( 13 | PatchMetadataService struct { 14 | Uris []string `json:"uris" binding:"required"` 15 | Patches []fs.MetadataPatch `json:"patches" binding:"required,dive"` 16 | } 17 | 18 | PatchMetadataParameterCtx struct{} 19 | ) 20 | 21 | func (s *PatchMetadataService) GetUris() []string { 22 | return s.Uris 23 | } 24 | 25 | func (s *PatchMetadataService) Patch(c *gin.Context) error { 26 | dep := dependency.FromContext(c) 27 | user := inventory.UserFromContext(c) 28 | m := manager.NewFileManager(dep, user) 29 | defer m.Recycle() 30 | 31 | uris, err := fs.NewUriFromStrings(s.Uris...) 32 | if err != nil { 33 | return serializer.NewError(serializer.CodeParamErr, "unknown uri", err) 34 | } 35 | 36 | return m.PatchMedata(c, uris, s.Patches...) 37 | } 38 | -------------------------------------------------------------------------------- /service/explorer/pin.go: -------------------------------------------------------------------------------- 1 | package explorer 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 5 | "github.com/cloudreve/Cloudreve/v4/inventory" 6 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 9 | "github.com/gin-gonic/gin" 10 | "github.com/samber/lo" 11 | ) 12 | 13 | type ( 14 | PinFileService struct { 15 | Uri string `json:"uri" binding:"required"` 16 | Name string `json:"name"` 17 | } 18 | PinFileParameterCtx struct{} 19 | ) 20 | 21 | // PinFileService pins new uri to sidebar 22 | func (service *PinFileService) PinFile(c *gin.Context) error { 23 | dep := dependency.FromContext(c) 24 | user := inventory.UserFromContext(c) 25 | userClient := dep.UserClient() 26 | 27 | uri, err := fs.NewUriFromString(service.Uri) 28 | if err != nil { 29 | return serializer.NewError(serializer.CodeParamErr, "unknown uri", err) 30 | } 31 | 32 | uriStr := uri.String() 33 | for _, pin := range user.Settings.Pined { 34 | if pin.Uri == uriStr { 35 | if pin.Name != service.Name { 36 | return serializer.NewError(serializer.CodeObjectExist, "uri already pinned with different name", nil) 37 | } 38 | 39 | return nil 40 | } 41 | } 42 | 43 | user.Settings.Pined = append(user.Settings.Pined, types.PinedFile{ 44 | Uri: uriStr, 45 | Name: service.Name, 46 | }) 47 | if err := userClient.SaveSettings(c, user); err != nil { 48 | return serializer.NewError(serializer.CodeDBError, "failed to save settings", err) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // UnpinFile removes uri from sidebar 55 | func (service *PinFileService) UnpinFile(c *gin.Context) error { 56 | dep := dependency.FromContext(c) 57 | user := inventory.UserFromContext(c) 58 | userClient := dep.UserClient() 59 | 60 | uri, err := fs.NewUriFromString(service.Uri) 61 | if err != nil { 62 | return serializer.NewError(serializer.CodeParamErr, "unknown uri", err) 63 | } 64 | 65 | uriStr := uri.String() 66 | user.Settings.Pined = lo.Filter(user.Settings.Pined, func(pin types.PinedFile, index int) bool { 67 | return pin.Uri != uriStr 68 | }) 69 | 70 | if err := userClient.SaveSettings(c, user); err != nil { 71 | return serializer.NewError(serializer.CodeDBError, "failed to save settings", err) 72 | } 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /service/node/response.go: -------------------------------------------------------------------------------- 1 | package node 2 | -------------------------------------------------------------------------------- /service/setting/response.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "github.com/cloudreve/Cloudreve/v4/ent" 5 | "github.com/cloudreve/Cloudreve/v4/inventory" 6 | "github.com/cloudreve/Cloudreve/v4/pkg/boolset" 7 | "github.com/cloudreve/Cloudreve/v4/pkg/hashid" 8 | "github.com/samber/lo" 9 | "time" 10 | ) 11 | 12 | type ListDavAccountResponse struct { 13 | Accounts []DavAccount `json:"accounts"` 14 | Pagination *inventory.PaginationResults `json:"pagination"` 15 | } 16 | 17 | func BuildListDavAccountResponse(res *inventory.ListDavAccountResult, hasher hashid.Encoder) *ListDavAccountResponse { 18 | return &ListDavAccountResponse{ 19 | Accounts: lo.Map(res.Accounts, func(item *ent.DavAccount, index int) DavAccount { 20 | return BuildDavAccount(item, hasher) 21 | }), 22 | Pagination: res.PaginationResults, 23 | } 24 | } 25 | 26 | type DavAccount struct { 27 | ID string `json:"id"` 28 | CreatedAt time.Time `json:"created_at"` 29 | Name string `json:"name"` 30 | Uri string `json:"uri"` 31 | Password string `json:"password"` 32 | Options *boolset.BooleanSet `json:"options"` 33 | } 34 | 35 | func BuildDavAccount(account *ent.DavAccount, hasher hashid.Encoder) DavAccount { 36 | return DavAccount{ 37 | ID: hashid.EncodeDavAccountID(hasher, account.ID), 38 | CreatedAt: account.CreatedAt, 39 | Name: account.Name, 40 | Uri: account.URI, 41 | Password: account.Password, 42 | Options: account.Options, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /service/share/response.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | import ( 4 | "net/url" 5 | 6 | "github.com/cloudreve/Cloudreve/v4/ent" 7 | "github.com/cloudreve/Cloudreve/v4/inventory" 8 | "github.com/cloudreve/Cloudreve/v4/inventory/types" 9 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" 10 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs" 11 | "github.com/cloudreve/Cloudreve/v4/pkg/hashid" 12 | "github.com/cloudreve/Cloudreve/v4/service/explorer" 13 | "github.com/samber/lo" 14 | ) 15 | 16 | type ListShareResponse struct { 17 | Shares []explorer.Share `json:"shares"` 18 | Pagination *inventory.PaginationResults `json:"pagination"` 19 | } 20 | 21 | func BuildListShareResponse(res *inventory.ListShareResult, hasher hashid.Encoder, base *url.URL, requester *ent.User, unlocked bool) *ListShareResponse { 22 | var infos []explorer.Share 23 | for _, share := range res.Shares { 24 | expired := inventory.IsValidShare(share) != nil 25 | shareName := share.Edges.File.Name 26 | if share.Edges.File.FileChildren == 0 && len(share.Edges.File.Edges.Metadata) >= 0 { 27 | // For files in trash bin, read the real name from metadata 28 | restoreUri, found := lo.Find(share.Edges.File.Edges.Metadata, func(m *ent.Metadata) bool { 29 | return m.Name == dbfs.MetadataRestoreUri 30 | }) 31 | if found { 32 | uri, err := fs.NewUriFromString(restoreUri.Value) 33 | if err == nil { 34 | shareName = uri.Name() 35 | } 36 | } 37 | } 38 | 39 | infos = append(infos, *explorer.BuildShare(share, base, hasher, requester, share.Edges.User, shareName, 40 | types.FileType(share.Edges.File.Type), unlocked, expired)) 41 | } 42 | 43 | return &ListShareResponse{ 44 | Shares: infos, 45 | Pagination: res.PaginationResults, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /service/user/info.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudreve/Cloudreve/v4/application/dependency" 6 | "github.com/cloudreve/Cloudreve/v4/ent" 7 | "github.com/cloudreve/Cloudreve/v4/inventory" 8 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs" 9 | "github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager" 10 | "github.com/cloudreve/Cloudreve/v4/pkg/hashid" 11 | "github.com/cloudreve/Cloudreve/v4/pkg/serializer" 12 | "github.com/gin-gonic/gin" 13 | "github.com/samber/lo" 14 | ) 15 | 16 | func GetUser(c *gin.Context) (*ent.User, error) { 17 | uid := hashid.FromContext(c) 18 | dep := dependency.FromContext(c) 19 | userClient := dep.UserClient() 20 | ctx := context.WithValue(c, inventory.LoadUserGroup{}, true) 21 | return userClient.GetByID(ctx, uid) 22 | } 23 | 24 | func GetUserCapacity(c *gin.Context) (*fs.Capacity, error) { 25 | user := inventory.UserFromContext(c) 26 | dep := dependency.FromContext(c) 27 | m := manager.NewFileManager(dep, user) 28 | defer m.Recycle() 29 | 30 | return m.Capacity(c) 31 | } 32 | 33 | type ( 34 | SearchUserService struct { 35 | Keyword string `form:"keyword" binding:"required,min=2"` 36 | } 37 | SearchUserParamCtx struct{} 38 | ) 39 | 40 | const resultLimit = 10 41 | 42 | func (s *SearchUserService) Search(c *gin.Context) ([]*ent.User, error) { 43 | dep := dependency.FromContext(c) 44 | userClient := dep.UserClient() 45 | res, err := userClient.SearchActive(c, resultLimit, s.Keyword) 46 | if err != nil { 47 | return nil, serializer.NewError(serializer.CodeDBError, "Failed to search user", err) 48 | } 49 | 50 | return res, nil 51 | } 52 | 53 | // ListAllGroups lists all groups. 54 | func ListAllGroups(c *gin.Context) ([]*ent.Group, error) { 55 | dep := dependency.FromContext(c) 56 | groupClient := dep.GroupClient() 57 | res, err := groupClient.ListAll(c) 58 | if err != nil { 59 | return nil, serializer.NewError(serializer.CodeDBError, "Failed to list all groups", err) 60 | } 61 | 62 | res = lo.Filter(res, func(g *ent.Group, index int) bool { 63 | return g.ID != inventory.AnonymousGroupID 64 | }) 65 | 66 | return res, nil 67 | } 68 | --------------------------------------------------------------------------------