├── .dockerignore ├── .github ├── FUNDING.yml ├── no-response.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── CODEOFCONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── app.json ├── cmd ├── root.go ├── server.go └── version.go ├── deployments └── zpan.service ├── docs ├── .nojekyll ├── CNAME ├── README.md ├── _navbar.md ├── _sidebar.md ├── cloud-platform.md ├── config.md ├── contributing.md ├── en │ ├── README.md │ ├── _sidebar.md │ ├── cloud-platform.md │ ├── config.md │ ├── contributing.md │ ├── faq.md │ ├── getting-started.md │ └── vernacular.md ├── faq.md ├── getting-started.md ├── index.html ├── static │ └── images │ │ ├── cloud-platform │ │ ├── image-20210712165603221.png │ │ ├── image-20210712170751044.png │ │ ├── image-20210712172027775.png │ │ ├── image-20210712172155195.png │ │ ├── image-20210712172346760.png │ │ ├── image-20210712172622541.png │ │ └── image-20210712172707803.png │ │ ├── set-storage │ │ ├── 000.jpg │ │ ├── 001.jpg │ │ ├── 002.jpg │ │ ├── 003.jpg │ │ ├── 004.jpg │ │ ├── 005.jpg │ │ ├── 006.jpg │ │ ├── 007.jpg │ │ ├── 008.jpg │ │ ├── 009.jpg │ │ ├── 010.jpg │ │ ├── 011.jpg │ │ └── 012.jpg │ │ └── wechat.png └── vernacular │ ├── _sidebar.md │ ├── images │ └── image.png │ ├── index.md │ ├── panel-baota.md │ └── provider-oss.md ├── go.mod ├── go.sum ├── hack └── gen │ └── main.go ├── heroku.yml ├── install.sh ├── internal ├── app │ ├── api │ │ ├── matter.go │ │ ├── recyclebin.go │ │ ├── router.go │ │ ├── share.go │ │ ├── storage.go │ │ ├── system.go │ │ ├── token.go │ │ ├── user.go │ │ ├── user_key.go │ │ └── wire.go │ ├── dao │ │ ├── dao.go │ │ ├── option.go │ │ ├── query.go │ │ ├── share.go │ │ ├── user.go │ │ └── user_key.go │ ├── entity │ │ ├── matter.go │ │ ├── matter_env.go │ │ ├── recycle.go │ │ ├── storage.go │ │ └── user_storage.go │ ├── model │ │ ├── base.go │ │ ├── option.go │ │ ├── share.go │ │ ├── user.go │ │ ├── user_key.go │ │ └── user_profile.go │ ├── repo │ │ ├── matter.go │ │ ├── matter_test.go │ │ ├── query │ │ │ ├── gen.go │ │ │ ├── zp_matter.gen.go │ │ │ ├── zp_recycle.gen.go │ │ │ ├── zp_storage.gen.go │ │ │ └── zp_storage_quota.gen.go │ │ ├── recyclebin.go │ │ ├── shared.go │ │ ├── shared_test.go │ │ ├── storage.go │ │ ├── user.go │ │ └── wire.go │ ├── server.go │ ├── service │ │ ├── mail.go │ │ ├── option.go │ │ ├── token.go │ │ ├── user.go │ │ └── user_key.go │ ├── usecase │ │ ├── authz │ │ │ ├── authz.go │ │ │ ├── authz.rego │ │ │ └── writer.go │ │ ├── storage │ │ │ ├── cloud_storage.go │ │ │ ├── cloud_storage_test.go │ │ │ └── storage.go │ │ ├── uploader │ │ │ ├── cloud_uploader.go │ │ │ ├── cloud_uploader_test.go │ │ │ ├── fake_uploader.go │ │ │ └── uploader.go │ │ ├── vfs │ │ │ ├── interfaces.go │ │ │ ├── recyclebin.go │ │ │ ├── vfs.go │ │ │ ├── vfs_jobs.go │ │ │ ├── vfs_test.go │ │ │ └── worker.go │ │ └── wire.go │ ├── wire.go │ └── wire_gen.go ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml ├── mock │ ├── matter.go │ ├── mock.go │ ├── recyclebin.go │ ├── storage.go │ └── user.go └── pkg │ ├── authed │ └── authed.go │ ├── bind │ ├── folder.go │ ├── matter.go │ ├── query.go │ ├── recycle.go │ ├── share.go │ ├── storage.go │ ├── token.go │ └── user.go │ ├── gormutil │ └── gorm.go │ ├── middleware │ ├── auth.go │ ├── auth_rbac.yml │ ├── installer.go │ ├── rbac_roles.go │ └── roles.go │ └── provider │ ├── provider.go │ ├── provider_cos.go │ ├── provider_kodo.go │ ├── provider_minio.go │ ├── provider_mock.go │ ├── provider_nos.go │ ├── provider_obs.go │ ├── provider_oss.go │ ├── provider_s3.go │ ├── provider_test.go │ ├── provider_us3.go │ └── provider_uss.go ├── main.go ├── pkg ├── nos │ ├── auth │ │ └── nosauth.go │ ├── config │ │ └── config.go │ ├── httpclient │ │ └── httpclient.go │ ├── logger │ │ └── logger.go │ ├── model │ │ ├── nosrequest.go │ │ └── nosresponse.go │ ├── nosclient │ │ └── nosclient.go │ ├── nosconst │ │ └── nosconst.go │ ├── noserror │ │ └── noserror.go │ ├── sample.go │ ├── test │ │ ├── fortest │ │ ├── testserver │ │ └── testserver2 │ ├── tools │ │ ├── cover.sh │ │ └── run_test.sh │ └── utils │ │ └── utils.go └── obs │ ├── auth.go │ ├── client.go │ ├── conf.go │ ├── const.go │ ├── convert.go │ ├── error.go │ ├── extension.go │ ├── http.go │ ├── log.go │ ├── model.go │ ├── pool.go │ ├── provider.go │ ├── temporary.go │ ├── trait.go │ ├── transfer.go │ └── util.go ├── quickstart └── docker-compose.yaml └── web ├── dist ├── css │ ├── chunk-046e590c.c120f90d.css │ ├── chunk-0e8dbb5f.0d875ebb.css │ ├── chunk-141f1d87.09f4af54.css │ ├── chunk-14d2e418.95c79dda.css │ ├── chunk-20d253c5.1ee10338.css │ ├── chunk-22dece4e.1d3af35b.css │ ├── chunk-26cc1f8f.a0ccf9af.css │ ├── chunk-2bcfbabe.66bfbc46.css │ ├── chunk-4fae512a.09f4af54.css │ ├── chunk-51b64701.09f4af54.css │ ├── chunk-5db82f0c.1d3af35b.css │ ├── chunk-6955f844.c4257cb3.css │ ├── chunk-77a33456.e0eb3850.css │ ├── chunk-77b2d504.439841c9.css │ ├── chunk-b66bdb3e.f01cebc8.css │ ├── chunk-bde9bbc0.6a417862.css │ ├── chunk-c35cc142.1579c98c.css │ ├── chunk-e11c2bda.be282ce8.css │ ├── chunk-e3ab30f8.8ad46891.css │ ├── chunk-vendors.c0118c1f.css │ └── index.d455708c.css ├── fonts │ ├── element-icons.535877f5.woff │ ├── element-icons.732389de.ttf │ ├── pdf.06fc6a29.woff2 │ ├── pdf.7928efbe.woff │ ├── pdf.8acc3f55.ttf │ └── pdf.fde29d48.eot ├── img │ ├── default-skin.b257fa9c.svg │ └── pdf.367cd90b.svg ├── index.html └── js │ ├── chunk-046e590c.40f42662.js │ ├── chunk-0e8dbb5f.9b6b84ce.js │ ├── chunk-141f1d87.6d003a26.js │ ├── chunk-14d2e418.4a453b10.js │ ├── chunk-20d253c5.579cd4f2.js │ ├── chunk-22dece4e.d90f78aa.js │ ├── chunk-26cc1f8f.1fb61c16.js │ ├── chunk-2bcfbabe.7115086a.js │ ├── chunk-2d0a4fde.a1892a24.js │ ├── chunk-2d0afa39.c1ef3224.js │ ├── chunk-2d0bce73.4499f935.js │ ├── chunk-2d0c5700.3d133d9f.js │ ├── chunk-2d0d76a6.8e1a29a4.js │ ├── chunk-2d0daeb3.58a4f15e.js │ ├── chunk-2d0e95df.0118b3f4.js │ ├── chunk-2d207759.e88b37b6.js │ ├── chunk-4fae512a.cb0504d5.js │ ├── chunk-51b64701.1821b57c.js │ ├── chunk-5aca0836.7a3a5f40.js │ ├── chunk-5db82f0c.e82b051a.js │ ├── chunk-6955f844.3bc93efb.js │ ├── chunk-77a33456.1ee007a9.js │ ├── chunk-77b2d504.cf0bdd9d.js │ ├── chunk-b66bdb3e.ec92846a.js │ ├── chunk-bde9bbc0.35bb7653.js │ ├── chunk-c35cc142.f968b023.js │ ├── chunk-c39e5b9a.f9db0cd4.js │ ├── chunk-e11c2bda.32ad1183.js │ ├── chunk-e3ab30f8.78491f1a.js │ ├── chunk-vendors.0649852f.js │ └── index.55c2e3a9.js ├── efs.go └── web.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | .coverprofile 11 | 12 | # Output 13 | build 14 | *.out 15 | .zpan.yml 16 | config.yml 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [saltbo] 4 | patreon: saltbo 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 14 5 | # Label requiring a response 6 | responseRequiredLabel: question 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response 10 | to our request for more information from the original author. With only the 11 | information that is currently in the issue, we don't have enough information 12 | to take action. Please reach out if you have or find the answers we need so 13 | that we can investigate further. -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v4 16 | with: 17 | go-version: 1.20.x 18 | - name: Check out code 19 | uses: actions/checkout@v3 20 | - name: Run Unit tests. 21 | run: make test 22 | - name: Upload Coverage report to CodeCov 23 | uses: codecov/codecov-action@v1 24 | with: 25 | # token: ${{secrets.CODECOV_TOKEN}} 26 | file: .coverprofile 27 | build: 28 | name: Build 29 | runs-on: ubuntu-latest 30 | needs: [test] 31 | steps: 32 | - name: Set up Go 33 | uses: actions/setup-go@v4 34 | with: 35 | go-version: 1.20.x 36 | - name: Check out code 37 | uses: actions/checkout@v3 38 | - name: Build 39 | run: make build -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - run: git fetch --force --tags 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: 1.20.x 20 | 21 | - name: DockerHub Login 22 | if: success() && startsWith(github.ref, 'refs/tags/') 23 | env: 24 | DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} 25 | run: | 26 | echo "${DOCKER_HUB_PASSWORD}" | docker login --username saltbo --password-stdin 27 | 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v4 30 | with: 31 | version: latest 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | .coverprofile 11 | 12 | # Output 13 | build 14 | *.out 15 | zpan.yml 16 | zpan.db 17 | config.yml 18 | 19 | # Editor auto file 20 | .idea 21 | .vscode 22 | 23 | 24 | # Dependency directories (remove the comment below to include it) 25 | # vendor/ 26 | 27 | # Go workspace file 28 | go.work* -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | dist: ./build/release 2 | before: 3 | hooks: 4 | - go mod download 5 | builds: 6 | - id: server 7 | main: ./main.go 8 | binary: bin/{{ .ProjectName }} 9 | ldflags: 10 | - -s -w 11 | - -X github.com/saltbo/{{ .ProjectName }}/cmd.release={{.Version}} 12 | - -X github.com/saltbo/{{ .ProjectName }}/cmd.commit={{.Commit}} 13 | - -X github.com/saltbo/{{ .ProjectName }}/cmd.repo={{.GitURL}} 14 | goos: 15 | - linux 16 | - darwin 17 | goarch: 18 | - amd64 19 | - arm64 20 | - arm 21 | goarm: 22 | - "6" 23 | - "7" 24 | 25 | release: 26 | prerelease: auto 27 | footer: | 28 | ## Docker Images 29 | 30 | - `saltbo/{{ .ProjectName }}:{{ .Version }}` 31 | 32 | 33 | dockers: 34 | - image_templates: 35 | - "saltbo/{{ .ProjectName }}:{{ .Version }}" 36 | # - "saltbo/{{ .ProjectName }}:v{{ .Major }}.{{ .Minor }}" 37 | # - "saltbo/{{ .ProjectName }}:latest" 38 | 39 | archives: 40 | - name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 41 | wrap_in_directory: true 42 | format: tar.gz 43 | format_overrides: # archive as zip on Windows 44 | - goos: windows 45 | format: zip 46 | files: 47 | - LICENSE 48 | - README.md 49 | - CHANGELOG.md 50 | - deployments/* 51 | - install.sh 52 | checksum: 53 | name_template: 'checksums.txt' 54 | snapshot: 55 | name_template: "{{ .Tag }}-next" 56 | changelog: 57 | sort: desc 58 | filters: 59 | exclude: 60 | - '^docs:' 61 | - '^test:' 62 | nfpms: 63 | - file_name_template: '{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 64 | homepage: https://github.com/saltbo/{{ .ProjectName }} 65 | description: A self-hosted cloud disk base on the cloud storage 66 | maintainer: Ambor 67 | license: MIT 68 | formats: 69 | - deb 70 | - rpm 71 | dependencies: 72 | - git 73 | recommends: 74 | - golang -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | - support responsive for different devices 11 | - support WebDAV 12 | - compression/decompression 13 | - Aria2 download into zpan 14 | - support onedrive && google drive 15 | 16 | ## [1.7.0] - 2023-08-06 17 | 18 | ### Added 19 | 20 | - 支持查看PDF文件 21 | - 支持查看编辑文本文件,如markdown,txt,yaml等文件 22 | - 新版上传器,支持一定程度上的后台上传(上传时可以切换页面但不能刷新页面) 23 | - S3地址支持Path-Style开关,开启后在使用minio时可以使用ip+port 24 | - 移除sqlite对CGO的依赖,支持armv6,armv7 #188 25 | - 分享时复制可以直接复制到密码 26 | 27 | ### Fixed 28 | 29 | - 修复上传无扩展名的文件报错问题 #185 30 | - 修复修改存储时报错的问题 #140 31 | - 修复回收站删除空文件夹时报错 #175 32 | - 修复分析列表数量过多会导致最后一个无法看到链接 33 | 34 | ## [1.6.0] - 2021-07-12 35 | 36 | ### Added 37 | 38 | - 增加对又拍云的支持 #55 39 | - 支持禁用某个云存储 #103 40 | - 支持禁用/删除某个用户 #113 41 | - 支持永久分享和取消分享 42 | - 前端页面右上角增加显示用户昵称显示 43 | - 管理员后台重置密码不再需要原密码 44 | - 增加开发者设置,开放API文档,支持开发者自行上传文件 45 | - 云平台的数据来源接入eplist,希望社区一起维护该项目 #84 46 | - 增加对ARM64的支持,可以允许在ARM架构的机器上了 #52 47 | 48 | ### Fixed 49 | 50 | - 修复用户无法重置密码的问题 #105 51 | - 修复上传中断/失败仍然占用了存储空间的问题 #78 52 | - 修复分享列表存在的越权问题 53 | - 修复上传多个文件时其中一个文件完成导致其他文件结束的问题 54 | - 修复部分RAR和DMG文件分享后无法下载的问题 55 | - 调整上传签名有效期,解决上传较慢时过期导致失败的问题 56 | - 调整自用域名仅用于查看或下载文件,不再用于上传 #111 57 | 58 | ## [1.5.0] - 2020-02-14 59 | 60 | ### Added 61 | 62 | - 增加了可视化的引导安装页面,帮助用户快速安装 63 | - 支持多存储,在后台管理员可以添加多个存储空间,在前台用户可以随意切换存储空间 64 | - 添加存储空间时支持自动设置 CORS (部分平台不支持),解决手动去云存储平台创建的麻烦 65 | - 存储空间支持网盘和外链盘两种类型,网盘关联的云存储设置为私有读,外链盘关联的云存储设置为公共读 66 | - 支持管理员在后台添加用户 67 | - 支持设置存储根路径和存储文件路径(支持使用变量) 68 | - 增加基本的后台管理功能 69 | - 增加 heroku 的支持,可以一键部署到 heroku 70 | 71 | ## [1.4.0] - 2020-10-13 72 | 73 | ### Added 74 | 75 | - redesign UI 76 | - support Recycle Bin 77 | - support update the user storage quota 78 | - support icon-layout for the file list 79 | - support auto https 80 | - support minio 81 | 82 | ## [1.3.0] - 2020-09-20 83 | 84 | ### Added 85 | 86 | - support search 87 | - support delete the folder [@XiangYu0777](https://github.com/XiangYu0777) 88 | - support preview for the audio and video 89 | - support aws-s3 && google storage 90 | - support i18n 91 | 92 | ### Changed 93 | 94 | - Fix: don't allow move the folder into itself [@holicc](https://github.com/holicc) 95 | - Fix: error display the file that upload failed for the sqlite3 driver 96 | - Fix: error display the folders of move dialog when the folder renamed 97 | - Improve the test coverage to 54% 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | By participating to this project, you agree to abide our [code of conduct](/CODEOFCONDUCT.md). 4 | 5 | ## Setup your machine 6 | 7 | `zpan` is written in [Go](https://golang.org/). 8 | 9 | Prerequisites: 10 | 11 | - `make` 12 | - [Go 1.20+](https://golang.org/doc/install) 13 | 14 | Clone `zpan` anywhere: 15 | 16 | ```sh 17 | $ git clone git@github.com:saltbo/zpan.git 18 | ``` 19 | 20 | Install the build and lint dependencies: 21 | 22 | ```sh 23 | $ make mod 24 | ``` 25 | 26 | A good way of making sure everything is all right is running the test suite: 27 | 28 | ```sh 29 | $ make test 30 | ``` 31 | 32 | ## Test your change 33 | 34 | You can create a branch for your changes and try to build from the source as you go: 35 | 36 | ```sh 37 | $ make build 38 | ``` 39 | 40 | Which runs all the linters and tests. 41 | 42 | ## Create a commit 43 | 44 | Commit messages should be well formatted, and to make that "standardized", we 45 | are using Conventional Commits. 46 | 47 | You can follow the documentation on 48 | [their website](https://www.conventionalcommits.org). 49 | 50 | ## Submit a pull request 51 | 52 | Push your branch to your `zpan` fork and open a pull request against the 53 | master branch. 54 | 55 | ## Financial contributions 56 | 57 | We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/zpan). 58 | Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. 59 | 60 | ## Credits 61 | 62 | ### Contributors 63 | 64 | Thank you to all the people who have already contributed to zpan! 65 | 66 | 67 | ### Backers 68 | 69 | Thank you to all our backers! [[Become a backer](https://opencollective.com/zpan#backer)] 70 | 71 | ### Sponsors 72 | 73 | Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/zpan#sponsor)) 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:10 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y ca-certificates telnet procps curl 5 | 6 | ENV APP_HOME /srv 7 | WORKDIR $APP_HOME 8 | 9 | COPY bin/zpan $APP_HOME 10 | 11 | CMD ["./zpan", "server"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all dep lint vet test test-coverage build clean 2 | 3 | # custom define 4 | PROJECT := zpan 5 | MAINFILE := main.go 6 | 7 | all: build 8 | 9 | mod: ## Get the dependencies 10 | go mod download 11 | 12 | lint: ## Lint Golang files 13 | @golangci-lint --version 14 | @golangci-lint run -D errcheck 15 | 16 | test: ## Run tests with coverage 17 | go test -coverprofile .coverprofile ./... 18 | go tool cover --func=.coverprofile 19 | 20 | coverage-html: ## show coverage by the html 21 | go tool cover -html=.coverprofile 22 | 23 | generate: ## generate the static assets 24 | go generate ./... 25 | 26 | build: mod ## Build the binary file 27 | go build -v -o build/bin/$(PROJECT) $(MAINFILE) 28 | 29 | swag: 30 | swag init -g internal/app/api/router.go --exclude client --parseDependency --parseDepth 1 --output internal/docs 31 | 32 | install: 33 | # 复制二进制文件 34 | # 复制默认配置文件 35 | 36 | clean: ## Remove previous build 37 | @rm -rf ./build 38 | 39 | help: ## Display this help screen 40 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/zpan server --config ./config.yml 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ZPan - Your Last disk on the cloud. 2 | =================================== 3 | 4 | [![](https://github.com/saltbo/zpan/workflows/build/badge.svg)](https://github.com/saltbo/zpan/actions?query=workflow%3Abuild) 5 | [![](https://codecov.io/gh/saltbo/zpan/branch/master/graph/badge.svg)](https://codecov.io/gh/saltbo/zpan) 6 | [![](https://img.shields.io/github/downloads/saltbo/zpan/total.svg)](https://github.com/saltbo/zpan/releases) 7 | [![](https://img.shields.io/docker/pulls/saltbo/zpan.svg)](https://hub.docker.com/r/saltbo/zpan) 8 | [![](https://img.shields.io/github/v/release/saltbo/zpan.svg)](https://github.com/saltbo/zpan/releases) 9 | [![](https://img.shields.io/github/license/saltbo/zpan.svg)](https://github.com/saltbo/zpan/blob/master/LICENSE) 10 | 11 | 12 | ## Overview 13 | - Not limited by server bandwidth 14 | - Support all cloud storage compatible with S3 protocol 15 | - Support file and folder management 16 | - Support file and folder sharing (accessible without logging in) 17 | - Support document preview and audio and video playback 18 | - Support multi-user storage space control 19 | - Support multiple languages 20 | 21 | 22 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/saltbo/zpan) 23 | 24 | 25 | ## Documentation 26 | - [简体中文](https://zpan.space) 27 | - [English](https://zpan.space/#/en/) 28 | 29 | 30 | ## Special thanks 31 | 32 | [![JetBrains](https://raw.githubusercontent.com/kainonly/ngx-bit/main/resource/jetbrains.svg)](https://www.jetbrains.com/?from=saltbo) 33 | 34 | Thanks for non-commercial open source development authorization by JetBrains 35 | 36 | ## Contributing 37 | See [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow. 38 | 39 | Thank you to all the people who already contributed to ZPan! 40 | 41 | 42 | [![Stargazers over time](https://starchart.cc/saltbo/zpan.svg)](https://starchart.cc/saltbo/zpan) 43 | 44 | ## License 45 | ZPan is under the GPL 3.0 license. See the [LICENSE](/LICENSE) file for details. 46 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ZPan", 3 | "description": "A self-host cloud disk base on the cloud storage", 4 | "repository": "https://github.com/saltbo/zpan", 5 | "logo": "https://node-js-sample.herokuapp.com/node.png", 6 | "keywords": [ 7 | "go", 8 | "zpan", 9 | "cloud-disk", 10 | "net-disk" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Ambor 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package cmd 23 | 24 | import ( 25 | "fmt" 26 | "os" 27 | 28 | "github.com/spf13/cobra" 29 | 30 | "github.com/spf13/viper" 31 | ) 32 | 33 | var cfgFile string 34 | 35 | // rootCmd represents the base command when called without any subcommands 36 | var rootCmd = &cobra.Command{ 37 | Use: "zpan", 38 | Short: "zpan is a cloud disk base on the cloud service.", 39 | Long: `Zpan is a cloud disk server 40 | built with love by saltbo and friends in Go. 41 | 42 | Complete documentation is available at https://saltbo.cn/zpan`, 43 | } 44 | 45 | // Execute adds all child commands to the root command and sets flags appropriately. 46 | // This is called by main.main(). It only needs to happen once to the rootCmd. 47 | func Execute() { 48 | if err := rootCmd.Execute(); err != nil { 49 | fmt.Println(err) 50 | os.Exit(1) 51 | } 52 | } 53 | 54 | func init() { 55 | cobra.OnInitialize(initConfig) 56 | 57 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is /etc/zpan/config.yml)") 58 | } 59 | 60 | // initConfig reads in config file and ENV variables if set. 61 | func initConfig() { 62 | if cfgFile != "" { 63 | // Use config file from the flag. 64 | viper.SetConfigFile(cfgFile) 65 | } else { 66 | viper.AddConfigPath("/etc/zpan") 67 | viper.SetConfigName("config") 68 | } 69 | 70 | viper.AutomaticEnv() // read in environment variables that match 71 | 72 | // If a config file is found, read it in. 73 | if err := viper.ReadInConfig(); err == nil { 74 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 75 | viper.Set("installed", true) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Ambor 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package cmd 23 | 24 | import ( 25 | "github.com/saltbo/zpan/internal/app" 26 | "github.com/spf13/cobra" 27 | "github.com/spf13/viper" 28 | ) 29 | 30 | // serverCmd represents the server command 31 | var serverCmd = &cobra.Command{ 32 | Use: "server", 33 | Short: "A cloud disk base on the cloud service.", 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | s := app.InitializeServer() 36 | return s.Run() 37 | }, 38 | } 39 | 40 | func init() { 41 | rootCmd.AddCommand(serverCmd) 42 | 43 | serverCmd.Flags().Int("port", 8222, "server port") 44 | 45 | _ = viper.BindPFlags(serverCmd.Flags()) 46 | } 47 | 48 | // func startTls(e *gin.Engine, conf *config.Config) { 49 | // tlsAddr := fmt.Sprintf(":%d", conf.Server.SSLPort) 50 | // if conf.TLS.Auto { 51 | // m := autocert.Manager{ 52 | // Prompt: autocert.AcceptTOS, 53 | // } 54 | // if err := os.MkdirAll(conf.TLS.CacheDir, 0700); err != nil { 55 | // log.Printf("autocert cache dir check failed: %s", err.Error()) 56 | // } else { 57 | // m.Cache = autocert.DirCache(conf.TLS.CacheDir) 58 | // } 59 | // if len(conf.Server.Domain) > 0 { 60 | // m.HostPolicy = autocert.HostWhitelist(conf.Server.Domain...) 61 | // } 62 | // srv := &http.Server{ 63 | // Addr: tlsAddr, 64 | // Handler: e, 65 | // TLSConfig: m.TLSConfig(), 66 | // } 67 | // go func() { 68 | // log.Printf("[rest server listen at %s]", srv.Addr) 69 | // if err := srv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { 70 | // log.Fatalln(err) 71 | // } 72 | // }() 73 | // 74 | // httputil.SetupGracefulStop(srv) 75 | // 76 | // } else { 77 | // srv := &http.Server{ 78 | // Addr: tlsAddr, 79 | // Handler: e, 80 | // } 81 | // go func() { 82 | // log.Printf("[rest server listen tls at %s]", srv.Addr) 83 | // if err := srv.ListenAndServeTLS(conf.TLS.CertPath, conf.TLS.CertkeyPath); err != nil && err != http.ErrServerClosed { 84 | // log.Fatalln(err) 85 | // } 86 | // }() 87 | // httputil.SetupGracefulStop(srv) 88 | // } 89 | // } 90 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | // RELEASE returns the release version 11 | release = "unknown" 12 | // REPO returns the git repository URL 13 | repo = "unknown" 14 | // COMMIT returns the short sha from git 15 | commit = "unknown" 16 | ) 17 | 18 | // versionCmd represents the version command 19 | var versionCmd = &cobra.Command{ 20 | Use: "version", 21 | Short: "print the version", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | fmt.Printf("release: %s, repo: %s, commit: %s", release, repo, commit) 24 | }, 25 | } 26 | 27 | func init() { 28 | rootCmd.AddCommand(versionCmd) 29 | 30 | // Here you will define your flags and configuration settings. 31 | 32 | // Cobra supports Persistent Flags which will work for this command 33 | // and all subcommands, e.g.: 34 | // versionCmd.PersistentFlags().String("foo", "", "A help for foo") 35 | 36 | // Cobra supports local flags which will only run when this command 37 | // is called directly, e.g.: 38 | // versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 39 | } 40 | -------------------------------------------------------------------------------- /deployments/zpan.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Zpan 3 | Documentation=https://github.com/saltbo/zpan 4 | After=network.target 5 | Wants=network.target 6 | 7 | [Service] 8 | WorkingDirectory=/usr/local/bin 9 | ExecStart=/usr/local/bin/zpan server 10 | Restart=on-abnormal 11 | RestartSec=5s 12 | KillMode=mixed 13 | 14 | StandardOutput=null 15 | StandardError=syslog 16 | 17 | [Install] 18 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | zpan.space -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### 欢迎 2 | !> 项目刚开始,文档尚不完善,欢迎帮忙PR 3 | 4 | ### 介绍 5 | ZPan致力于打造一款不限速的网盘系统,因此我们采用客户端直连云存储的方式进行设计。 6 | 7 | 目前ZPan支持所有兼容S3协议的云存储平台,您可以选用您熟悉的平台来驱动ZPan。 8 | 9 | [在线体验](http://zpan.saltbo.cn)(体验账号:demo,密码:demo) 10 | 11 | ### 他是如何工作的? 12 | 13 | ZPan本质上是一个URL签名服务器+可视化的文件浏览器。 14 | 15 | 因为我们采用直链的方式进行上传下载,所以为了保证上传下载的安全性,客户端用来上传下载的URL均需要服务端进行签名。 16 | 17 | 然后为了能给方便的管理用户上传的文件,我们就需要开发一个可视化的伪文件系统来进行文件管理。 18 | 19 | - [saltbo/zpan](https://github.com/saltbo/zpan) 20 | - [saltbo/zpan-front](https://github.com/saltbo/zpan-front) 21 | 22 | ### 特色 23 | - 完全不受服务器带宽限制 24 | - 支持所有兼容S3协议的云存储 25 | - 支持文件及文件夹管理 26 | - 支持文件及文件夹分享(未登录可访问) 27 | - 支持文档预览及音视频播放 28 | - 支持多用户存储空间控制 29 | - 支持多语言 30 | 31 | ### 为什么不是...? 32 | 33 | #### NextCloud 34 | NextCloud是非常好用的网盘系统,可以说是这个领域的前辈了。但是由于它诞生的比较早,在设计上它是基于本地文件系统进行存储的。这就意味着如果您使用NextCloud在一台一兆带宽的服务器上搭建一个网盘,那么网盘的上传下载速度上限就只有一兆,如果您想提升速度就只能给服务器升级带宽,这将是很大的成本。 35 | 36 | 当然,有人可能会说NextCloud也可以用云存储啊。但其实它是通过将云存储挂载成本地磁盘的方式进行使用的,还是无法解决上传下载速度受限于服务器带宽的问题。 37 | 38 | #### Cloudreve 39 | 40 | Cloudreve是我在研发ZPan之前找到的唯一满足我需求(上传下载不受带宽限制)的产品。但是当时Cloudreve是基于PHP开发的,我有点嫌弃它部署起来比较麻烦,所以就想着用Golang自己实现一个。但是由于一些原因,中途搁置了一年多,当我重新开始搞ZPan且已经差不多搞完的时候才发现在这一年中Cloudreve也使用Golang进行重构了。 41 | 42 | 不可否认Cloudreve在功能上是比ZPan更加多的,ZPan在功能上会比较克制,因为我始终认为功能不是越多越好。所以如果您发现ZPan不满足您的需求,也可以去试试Cloudreve。 43 | 44 | #### 蓝眼云盘 45 | 46 | 蓝眼云盘也是我早期在找网盘产品的时候找到的,总体来说他符合我的设想,简单好用。但很遗憾,他也属于传统网盘,我和他的开发者有过交流,他们并没有打算支持云存储。 47 | 48 | #### Z-File 49 | 50 | Z-File是一个在线文件目录的程序, 支持各种对象存储和本地存储, 使用定位是个人放常用工具下载, 或做公共的文件库. 不会向多账户方向开发. 51 | 52 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | * [中文](/) 2 | * [English](/en/) -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [介绍](/) 2 | * [快速开始](/getting-started.md) 3 | * [配置文件](/config.md) 4 | * [云存储](/cloud-platform.md) 5 | * [贡献代码](/contributing.md) 6 | * [白话文教程](/vernacular/index.md) 7 | * [常见问题](/faq.md) -------------------------------------------------------------------------------- /docs/cloud-platform.md: -------------------------------------------------------------------------------- 1 | ## S3协议平台 2 | 3 | - 阿里云OSS 4 | - 腾讯云COS 5 | - 七牛云Kodo 6 | - UCloud(需手动自动设置CORS) 7 | - 华为云OBS 8 | - 网易云NOS(需手动自动设置CORS) 9 | - 亚马逊S3 10 | - MinIO 11 | 12 | > 路径:管理后台-存储管理-创建存储 13 | 14 | ### 基础配置 15 | 16 | image-20210712165603221 17 | 18 | 1. 网盘和外链盘的区别是:外链盘可以直接拿到永久外链,同时它没有分享和回收站的功能 19 | 2. 名称是在zpan中该存储的名字,同时也是路由地址,所以只支持英文 20 | 3. 云平台的数据来源于 https://github.com/eplist/eplist ,欢迎PR 21 | 4. 接入点的数据同样源于eplist,选择一个云平台后会自动给出该平台的所有Endpoint 22 | 23 | ### 高级配置 24 | ![image-20210712170751044](./static/images/cloud-platform/image-20210712170751044.png) 25 | 26 | 1. 高级配置默认都可为空 27 | 2. 标题可以是中文,用于在页面顶部的导航栏显示 28 | 3. 自有域名即用来访问/下载资源的域名 29 | 4. 存储根路径指的是在云存储里的存储路径,默认是根目录,指定一个前缀,可以实现共用一个bucket的场景 30 | 5. 文件存储路径指的是上传的文件在云存储里的存储路径,默认是$NOW_DATE/$RAND_16KEY.$RAW_EXT 31 | 6. 可以看到我们支持一些系统变量,通过这些变量你可以设置自己的路径规则 32 | 33 | ### CORS配置 34 | 35 | - Origin: http://your-domain 36 | - AllowMethods: PUT 37 | - AllowHeaders: content-type,content-disposition,x-amz-acl 38 | 39 | 40 | 41 | ## MinIO 42 | 43 | 基于MinIO可以快速搭建自己的私有云。 44 | 45 | 需要注意的是,我们仅支持Virtual Hosted-Style模式的S3协议,所以在MinIO搭建时需要注意开启Virtual Hosted-Style。 46 | 47 | 启用的方式很简单,即设置环境变量MINIO_DOMAIN=endpoint.example.com 48 | 49 | 当您创建一个bucket名叫zpan时,它的完整域名是zpan.endpoint.example.com 50 | 51 | 但是,注意,**zpan中需要您填写的接入点是不包含bucket的,所以您应该填写endpoint.example.com** 52 | 53 | 54 | 55 | #### 参考文档: 56 | 57 | - https://docs.min.io/docs/minio-server-configuration-guide.html 58 | - https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTAPI.html 59 | 60 | 61 | 62 | 63 | 64 | ## 又拍云 65 | 66 | !> 又拍云不兼容s3协议,所以和其他平台有一些区别,需要特别注意 67 | 68 | 1. Endpoint填写又拍云默认分配的加速域名(仅供测试那个) 69 | 2. AccessKey为操作员名称,AccessSecret为操作员密码 70 | 3. **如果是网盘类型,需要将Token防盗链的秘钥设置为操作员的密码** 71 | 72 | ![image-20210712172027775](./static/images/cloud-platform/image-20210712172027775.png)![image-20210712172346760](./static/images/cloud-platform/image-20210712172346760.png) 73 | 74 | ![image-20210712172707803](./static/images/cloud-platform/image-20210712172707803.png) -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # 配置文件 2 | 3 | 从v1.5.0版本开始,我们启用了可视化的安装流程,初次安装完成后会生成config.yml的配置文件。如果您需要修改端口或者数据库配置,可以打开该文件进行编辑修改。 4 | 5 | ## port 6 | Http端口,默认为8222 7 | 8 | ## database 9 | 这里定义了ZPan的数据库驱动 10 | ```yaml 11 | database: 12 | driver: sqlite3 13 | dsn: zpan.db 14 | ``` 15 | 16 | ### driver 17 | 我们支持四种数据库驱动,您可以根据需求进行选择 18 | 19 | - sqlite3 20 | - mysql 21 | - postgres 22 | - mssql 23 | 24 | ### dsn 25 | 不用的驱动对应的dsn也是不一样的,这里我们分别给出每种驱动的dsn格式 26 | 27 | | driver | dsn | 28 | | ---- | ---- | 29 | | sqlite3 | zpan.db | 30 | | mysql | user:pass@tcp(127.0.0.1:3306)/zpan?charset=utf8mb4&parseTime=True&loc=Local | 31 | | postgres | user=zpan password=zpan dbname=zpan port=9920 sslmode=disable TimeZone=Asia/Shanghai | 32 | | mssql | sqlserver://zpan:LoremIpsum86@localhost:9930?database=zpan | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | ### 贡献代码 2 | 3 | !> 参与该项目,即表示您同意遵守我们的[行为准则](/CODEOFCONDUCT.md)。 4 | 5 | ### 安装环境 6 | 7 | `zpan` 基于[Go](https://golang.org/)进行开发. 8 | 9 | 依赖: 10 | 11 | - `make` 12 | - [Go 1.16+](https://golang.org/doc/install) 13 | 14 | 克隆源码: 15 | 16 | ```sh 17 | $ git clone git@github.com:saltbo/zpan.git 18 | ``` 19 | 20 | 安装构建依赖: 21 | 22 | ```sh 23 | $ make dep 24 | ``` 25 | 26 | 运行单元测试 27 | 28 | ```sh 29 | $ make test 30 | ``` 31 | 32 | ### 测试你的修改 33 | 34 | 您可以为更改创建分支,并尝试从源代码进行构建: 35 | 36 | ```sh 37 | $ make build 38 | ``` 39 | 40 | ### 创建一个提交 41 | 42 | 提交消息的格式应正确,并使其“标准化”,我们正在使用常规提交。 43 | 44 | 您可以按照以下文档操作 45 | [their website](https://www.conventionalcommits.org). 46 | 47 | ### 提交一个Pull Request 48 | 将分支推到您的`zpan`分支,然后对主分支打开一个拉取请求。 -------------------------------------------------------------------------------- /docs/en/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [Introduction](/en/) 2 | * [Getting Started](/en/getting-started.md) 3 | * [Config](/en/config.md) 4 | * [Cloud Storage](/en/cloud-platform.md) 5 | * [Contributing](/en/contributing.md) 6 | * [Vernacular](/en/vernacular.md) 7 | * [FAQ](/en/faq.md) -------------------------------------------------------------------------------- /docs/en/cloud-platform.md: -------------------------------------------------------------------------------- 1 | > Here we provide different endpoint samples for each major platform, you need to change `my-zpan-bucket` to your own bucket name 2 | 3 | ## Endpoint 4 | 5 | - AmazonS3:`s3.ap-northeast-1.amazonaws.com` 6 | - TencentCOS:`cos.ap-shanghai.myqcloud.com` 7 | - AliyunOSS:`oss-cn-zhangjiakou.aliyuncs.com` 8 | - QiniuKodo:`s3-cn-east-1.qiniucs.com` 9 | - GoogleStorage:`storage.googleapis.com` 10 | 11 | ## CORS 12 | 13 | - Origin: http://your-domain 14 | - AllowMethods: PUT 15 | - AllowHeaders: content-type,content-disposition,x-amz-acl 16 | 17 | ### S3 18 | The permissions of s3 are more complicated, and I have not conducted detailed tests. At present, I directly turn off the option of `Block public access to buckets and objects granted through new access control lists (ACLs)`; then configure the following CORS Just fill in and save. 19 | ```json 20 | [ 21 | { 22 | "AllowedOrigins": [ 23 | "http://your-domain" 24 | ], 25 | "AllowedMethods": [ 26 | "PUT" 27 | ], 28 | "AllowedHeaders": [ 29 | "content-type", 30 | "content-disposition", 31 | "x-amz-acl" 32 | ], 33 | "ExposeHeaders": [] 34 | } 35 | ] 36 | ``` 37 | 38 | ### Storage 39 | Since Google Cloud Storage does not provide a visual configuration interface, only a command line tool, so we can use CloudShell to operate, the specific commands are as follows: 40 | ```bash 41 | echo '[{"origin":["*"],"method":["PUT"],"responseHeader":["content-type","content-disposition","x-amz-acl"]}]' > cors.json 42 | gsutil cors set cors.json gs://your-bucket-name 43 | ``` 44 | 45 | ### Others 46 | OSS, COS, Kodo and other visualizations are all very good, so don’t talk nonsense, if you really don’t know how to do it, you can watch [Vernacular](/vernacular) -------------------------------------------------------------------------------- /docs/en/contributing.md: -------------------------------------------------------------------------------- 1 | ### Contributing 2 | 3 | By participating to this project, you agree to abide our [code of conduct](/CODEOFCONDUCT.md). 4 | 5 | #### Setup your machine 6 | 7 | `zpan` is written in [Go](https://golang.org/). 8 | 9 | Prerequisites: 10 | 11 | - `make` 12 | - [Go 1.13+](https://golang.org/doc/install) 13 | 14 | Clone `zpan` anywhere: 15 | 16 | ```sh 17 | $ git clone git@github.com:saltbo/zpan.git 18 | ``` 19 | 20 | Install the build and lint dependencies: 21 | 22 | ```sh 23 | $ make dep 24 | ``` 25 | 26 | A good way of making sure everything is all right is running the test suite: 27 | 28 | ```sh 29 | $ make test 30 | ``` 31 | 32 | ### Test your change 33 | 34 | You can create a branch for your changes and try to build from the source as you go: 35 | 36 | ```sh 37 | $ make build 38 | ``` 39 | 40 | Which runs all the linters and tests. 41 | 42 | #### Create a commit 43 | 44 | Commit messages should be well formatted, and to make that "standardized", we 45 | are using Conventional Commits. 46 | 47 | You can follow the documentation on 48 | [their website](https://www.conventionalcommits.org). 49 | 50 | #### Submit a pull request 51 | 52 | Push your branch to your `zpan` fork and open a pull request against the 53 | master branch. -------------------------------------------------------------------------------- /docs/en/faq.md: -------------------------------------------------------------------------------- 1 | ### FAQ 2 | 3 | To be collected 4 | 5 | ### Feedback 6 | If the problem you encounter is not within the above range, please create an [issue](https://github.com/saltbo/zpan/issues) on GitHub for feedback. 7 | 8 | If you find any defects in the process of use, or have a new demand proposal, you are also welcome to submit an [issue](https://github.com/saltbo/zpan/issues). 9 | 10 | ### Contact 11 | - [Telegram](https://t.me/zzpan) 12 | 13 | Or contact the developer: 14 | 15 | Email:saltbo@foxmail.com -------------------------------------------------------------------------------- /docs/en/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Linux 2 | ```bash 3 | # install service 4 | curl -sSLf https://dl.saltbo.cn/install.sh | sh -s zpan 5 | 6 | # start service 7 | systemctl start zpan 8 | 9 | # check service status 10 | systemctl status zpan 11 | 12 | # set boot up 13 | systemctl enable zpan 14 | ``` 15 | 16 | ## Docker 17 | ```bash 18 | docker run -it -p 8222:8222 -v /etc/zpan:/etc/zpan --name zpan saltbo/zpan 19 | ``` 20 | 21 | ## CORS 22 | 23 | !> Since we use browser-side direct transmission, there are cross-domain issues, please make the following cross-domain configuration 24 | 25 | - Origin: http://your-domain 26 | - AllowMethods: PUT 27 | - AllowHeaders: content-type,content-disposition,x-amz-acl 28 | 29 | ### Usage 30 | visit http://localhost:8222 31 | -------------------------------------------------------------------------------- /docs/en/vernacular.md: -------------------------------------------------------------------------------- 1 | ### Vernacular Course 2 | 3 | - How to run ZPan based on Alibaba Cloud OSS(Recruiting) 4 | - How to run ZPan based on Qiniu Kodo(Recruiting) 5 | - How to run ZPan based on Tencent COS(Recruiting) 6 | - How to run ZPan based on Amazon S3(Recruiting) 7 | - How to run ZPan based on Google Cloud Storage(Recruiting) -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | **一、Network Error是什么原因?** 4 | 5 | 答:如果您的网络状况正常,那么可能是没有进行跨域设置。请检查您使用的云平台是否需要手动设置跨域 6 | 7 | **二、Docker安装之后如何修改配置文件?** 8 | 9 | 答:可以把docker里的默认配置文件复制到宿主机上,在宿主机上修改调整之后再挂载到docker里。下面的命令供参考 10 | 11 | ```bash 12 | docker run -it zpan cat /etc/zpan/zpan.yml > /etc/zpan/zpan.yml 13 | vi /etc/zpan/zpan.yml 14 | docker run -it -v /etc/zpan:/etc/zpan zpan 15 | ``` 16 | 17 | **三、为什么接入点不支持IP+端口** 18 | 19 | ~~答:因为我们仅支持virtual-host-style,系统会自动把bucket名称拼接到接入点上,如果使用ip拼上去就错了。详见https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTAPI.html~~ 20 | 21 | 答:从v1.7.0版本已经支持是否开启PathStyle,开启后即可使用IP+端口形式 22 | 23 | **四、为什么不支持Windows系统?** 24 | 25 | 答:实际上Mac也没有提供二进制包,主要是考虑ZPan是一个服务端程序,再加上ZPan用到的一些依赖在支持多平台的情况下打包遇到一些麻烦,所以目前仅提供了一个Linux版本的Release,后续我们会考虑支持多平台。 26 | 27 | ## 用户反馈 28 | 如果您遇到的问题不再以上范围内,请到GitHub上创建一个[issue](https://github.com/saltbo/zpan/issues)进行反馈。 29 | 30 | 如果您在使用过程中发现了什么缺陷,或是有新的需求提议,也欢迎提交[issue](https://github.com/saltbo/zpan/issues)。 31 | 32 | ## 联系 33 | 34 | - Telegram: https://t.me/zpanchannel 35 | - 邮箱:saltbo@foxmail.com 36 | - 微信:saltbo 37 | 38 | wechat 39 | 40 | 扫码备注ZPan进入微信群,和我一起完善这个产品。 41 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | ### Linux 4 | ```bash 5 | # 安装服务 6 | curl -sSLf https://dl.saltbo.cn/install.sh | sh -s zpan 7 | 8 | # 启动服务 9 | systemctl start zpan 10 | 11 | # 查看服务状态 12 | systemctl status zpan 13 | 14 | # 设置开机启动 15 | systemctl enable zpan 16 | 17 | # 查看日志 18 | journalctl -xe -u zpan -f 19 | ``` 20 | 21 | ### Docker 22 | ```bash 23 | docker run -it -p 8222:8222 -v /etc/zpan:/etc/zpan --name zpan saltbo/zpan 24 | ``` 25 | 26 | ### StartWithMinIO 27 | ```bash 28 | mkdir localzpan && cd localzpan 29 | curl -L https://raw.githubusercontent.com/saltbo/zpan/master/quickstart/docker-compose.yaml -o docker-compose.yaml 30 | docker-compose up -d 31 | ``` 32 | 33 | ## Usage 34 | 35 | visit http://localhost:8222 36 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Zpan 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 |
15 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/static/images/cloud-platform/image-20210712165603221.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/cloud-platform/image-20210712165603221.png -------------------------------------------------------------------------------- /docs/static/images/cloud-platform/image-20210712170751044.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/cloud-platform/image-20210712170751044.png -------------------------------------------------------------------------------- /docs/static/images/cloud-platform/image-20210712172027775.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/cloud-platform/image-20210712172027775.png -------------------------------------------------------------------------------- /docs/static/images/cloud-platform/image-20210712172155195.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/cloud-platform/image-20210712172155195.png -------------------------------------------------------------------------------- /docs/static/images/cloud-platform/image-20210712172346760.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/cloud-platform/image-20210712172346760.png -------------------------------------------------------------------------------- /docs/static/images/cloud-platform/image-20210712172622541.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/cloud-platform/image-20210712172622541.png -------------------------------------------------------------------------------- /docs/static/images/cloud-platform/image-20210712172707803.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/cloud-platform/image-20210712172707803.png -------------------------------------------------------------------------------- /docs/static/images/set-storage/000.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/000.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/001.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/002.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/003.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/004.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/005.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/005.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/006.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/006.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/007.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/007.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/008.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/008.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/009.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/009.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/010.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/010.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/011.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/011.jpg -------------------------------------------------------------------------------- /docs/static/images/set-storage/012.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/set-storage/012.jpg -------------------------------------------------------------------------------- /docs/static/images/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/static/images/wechat.png -------------------------------------------------------------------------------- /docs/vernacular/_sidebar.md: -------------------------------------------------------------------------------- 1 | 2 | * [介绍](/) 3 | * [快速开始](/getting-started.md) 4 | * [配置文件](/config.md) 5 | * [云存储](/cloud-platform.md) 6 | * [贡献代码](/contributing.md) 7 | * [白话文教程](/vernacular/index.md) 8 | * [手把手教你在宝塔面板安装 Zpan](/vernacular/panel-baota.md) 9 | * [手把手教你申请阿里云OSS存储桶](/vernacular/provider-oss.md) 10 | * [常见问题](/faq.md) 11 | -------------------------------------------------------------------------------- /docs/vernacular/images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/docs/vernacular/images/image.png -------------------------------------------------------------------------------- /docs/vernacular/index.md: -------------------------------------------------------------------------------- 1 | # 白话文教程 2 | 3 | - [手把手教你在宝塔面板安装 Zpan](/vernacular/panel-baota.md) 4 | - [手把手教你配置阿里云OSS存储桶](/vernacular/provider-oss.md) 5 | - 手把手教你配置七牛Kodo(招募中) 6 | - 手把手教你配置腾讯COS(招募中) 7 | - 手把手教你配置亚马逊S3(招募中) 8 | - 手把手教你配置谷歌云Storage(招募中) 9 | -------------------------------------------------------------------------------- /docs/vernacular/panel-baota.md: -------------------------------------------------------------------------------- 1 | # 使用宝塔面板安装Zpan 2 | 3 | ## 前提 4 | 5 | 安装宝塔面板,前往[宝塔面板官网](https://www.bt.cn/u/Z2PlVN),选择对应的脚本下载安装。 6 | 7 | 宝塔版本要求:9.2.0+ 8 | 9 | ## 宝塔面板一键部署 Zpan 10 | 11 | 1. 登录宝塔面板,在菜单栏中点击 Docker,根据提示安装 Docker 和 Docker Compose 服务。 12 | 13 | ![alt text](images/image.png) 14 | 15 | 在宝塔面板安装 Docker 服务,若已有则跳过。 16 | 17 | 2. 在Docker-应用商店查询到 Zpan,点击安装 18 | 19 | ![000](../static/images/set-storage/000.jpg) 20 | 21 | 设置域名等基本信息,点击确定 22 | 23 | 3. 根据你设置的访问路径进入安装页面(域名地址:8222 / ip地址:8222)。 24 | 25 | ![001](../static/images/set-storage/001.jpg) 26 | 27 | 出现以上页面表示,部署成功,等待进行系统初始化。此时我们需要一个数据库。 28 | 29 | ## 系统初始化 30 | 31 | ### 创建数据库 32 | 33 | > 已有数据库可跳过此步 34 | 35 | 在宝塔中创建一个名为zpan的数据库,记住用户名和密码。 36 | 37 | ![004](../static/images/set-storage/004.jpg) 38 | 39 | 40 | ### 配置数据库 41 | 42 | 在安装页面填写对应数据库的信息,我们需要填写一个DSN。MySQL的DSN格式如下: 43 | 44 | 格式:`数据库用户名:数据库密码@tcp(数据库地址:端口)/数据库名?charset=utf8mb4&parseTime=True&loc=Local` 45 | 46 | 例子:`user:pass@tcp(127.0.0.1:3306)/zpan?charset=utf8mb4&parseTime=True&loc=Local` 47 | 48 | 更多数据库详见:[数据库配置](/config?id=dsn) 49 | 50 | ### 设置管理员账号 51 | 52 | 设置好管理员账号,点击开始安装即可 53 | 54 | ![002](../static/images/set-storage/002.jpg) 55 | 56 | 如果有报错 请检查 数据库配置信息 57 | 58 | ### 安装成功 59 | 60 | ![003](../static/images/set-storage/003.jpg) 61 | 62 | -------------------------------------------------------------------------------- /docs/vernacular/provider-oss.md: -------------------------------------------------------------------------------- 1 | 2 | ### 手把手教你申请阿里云OSS存储桶 3 | 4 | 5 | 6 | ① **登录阿里云,右上角头像选择 ==访问控制==** 7 | 8 | ![005](../static/images/set-storage/005.jpg) 9 | 10 | 11 | ② **新建用户** 12 | 13 | 14 | ![006](../static/images/set-storage/006.jpg) 15 | 16 | ![007](../static/images/set-storage/007.jpg) 17 | 18 | 19 | ③ **保存AccessKey ID AccessKey Secret备用** 20 | 21 | ![008](../static/images/set-storage/008.jpg) 22 | 23 | ④ **创建对象存储** 24 | 25 | 打开阿里云[对象存储](https://oss.console.aliyun.com/) 26 | 27 | ![009](../static/images/set-storage/009.jpg) 28 | 29 | 配置存储桶(Bucket) 30 | 31 | ![010](../static/images/set-storage/010.jpg) 32 | 33 | 进入存储桶,记住Bucket名称(存储桶名称)和Endpoint(地域节点) 34 | 35 | ![011](../static/images/set-storage/011.jpg) 36 | 37 | ⑤ **Zpan创建存储** 38 | 39 | 登录zpan后,点头像进入管理后台,右上角创建存储,按提示填入信息 40 | 41 | ![012](../static/images/set-storage/012.jpg) 42 | 43 | > [!NOTE] 44 | > 注意:外链盘可以直接拿到永久外链,同时它没有分享和回收站的功能 45 | 46 | 47 | -------------------------------------------------------------------------------- /hack/gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/saltbo/zpan/internal/app/entity" 5 | "gorm.io/gen" 6 | ) 7 | 8 | func main() { 9 | g := gen.NewGenerator(gen.Config{ 10 | OutPath: "./internal/app/repo/query", 11 | Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode 12 | }) 13 | 14 | // Generate basic type-safe DAO API for struct `model.User` following conventions 15 | g.ApplyBasic(entity.Storage{}, entity.Matter{}, entity.RecycleBin{}, entity.UserStorage{}) 16 | 17 | // Generate the code 18 | g.Execute() 19 | } 20 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | languages: 3 | - go 4 | 5 | run: 6 | web: bin/zpan 7 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | shell_dir=$(cd "$(dirname "$0")" || exit;pwd) 4 | if [ ! -d "${shell_dir}/bin" ]; then 5 | echo "not found bin files" 6 | exit 7 | fi 8 | 9 | project="zpan" 10 | unameOut="$(uname -s)" 11 | config_dir="/etc/${project}" 12 | test ! -d "${config_dir}" && mkdir "${config_dir}" 13 | cp "${shell_dir}/bin/${project}" /usr/local/bin 14 | cp -r "${shell_dir}"/deployments/. "${config_dir}" 15 | if [ "${unameOut}" = "Linux" ]; then 16 | cp "${shell_dir}/deployments/${project}".service /etc/systemd/system 17 | fi -------------------------------------------------------------------------------- /internal/app/api/recyclebin.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/saltbo/gopkg/ginutil" 6 | "github.com/saltbo/zpan/internal/app/repo" 7 | "github.com/saltbo/zpan/internal/app/usecase/vfs" 8 | "github.com/saltbo/zpan/internal/pkg/authed" 9 | "github.com/saltbo/zpan/internal/pkg/bind" 10 | ) 11 | 12 | type RecycleBinResource struct { 13 | rbr repo.RecycleBin 14 | rbf vfs.RecycleBinFs 15 | } 16 | 17 | func NewRecycleBinResource(rbr repo.RecycleBin, rbf vfs.RecycleBinFs) *RecycleBinResource { 18 | return &RecycleBinResource{rbr: rbr, rbf: rbf} 19 | } 20 | 21 | func (rs *RecycleBinResource) Register(router *gin.RouterGroup) { 22 | router.GET("/recycles", rs.findAll) 23 | router.PUT("/recycles/:alias", rs.recovery) 24 | router.DELETE("/recycles/:alias", rs.delete) 25 | router.DELETE("/recycles", rs.clean) 26 | } 27 | 28 | func (rs *RecycleBinResource) findAll(c *gin.Context) { 29 | p := new(bind.QueryRecycle) 30 | if err := c.BindQuery(p); err != nil { 31 | ginutil.JSONBadRequest(c, err) 32 | return 33 | } 34 | 35 | opts := &repo.RecycleBinFindOptions{ 36 | QueryPage: repo.QueryPage(p.QueryPage), 37 | Uid: authed.UidGet(c), 38 | Sid: p.Sid, 39 | } 40 | list, total, err := rs.rbr.FindAll(c, opts) 41 | if err != nil { 42 | ginutil.JSONServerError(c, err) 43 | return 44 | } 45 | 46 | ginutil.JSONList(c, list, total) 47 | } 48 | 49 | func (rs *RecycleBinResource) recovery(c *gin.Context) { 50 | alias := c.Param("alias") 51 | if err := rs.rbf.Recovery(c, alias); err != nil { 52 | ginutil.JSONServerError(c, err) 53 | return 54 | } 55 | 56 | ginutil.JSON(c) 57 | } 58 | 59 | func (rs *RecycleBinResource) delete(c *gin.Context) { 60 | alias := c.Param("alias") 61 | if err := rs.rbf.Delete(c, alias); err != nil { 62 | ginutil.JSONServerError(c, err) 63 | return 64 | } 65 | 66 | ginutil.JSON(c) 67 | } 68 | 69 | func (rs *RecycleBinResource) clean(c *gin.Context) { 70 | if err := rs.rbf.Clean(c, ginutil.QueryInt64(c, "sid"), authed.UidGet(c)); err != nil { 71 | ginutil.JSONServerError(c, err) 72 | return 73 | } 74 | 75 | ginutil.JSON(c) 76 | } 77 | -------------------------------------------------------------------------------- /internal/app/api/router.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/saltbo/gopkg/ginutil" 6 | "github.com/saltbo/zpan/internal/app/usecase/authz" 7 | _ "github.com/saltbo/zpan/internal/docs" 8 | ) 9 | 10 | // @title zpan 11 | // @description zpan apis 12 | // @version 1.0.0 13 | 14 | // @BasePath /api/ 15 | // @securitydefinitions.oauth2.application OAuth2Application 16 | // @scope.matter Grants matter access and write 17 | // @scope.admin Grants read and write access to administrative information 18 | // @tokenUrl /api/tokens 19 | // @name Authorization 20 | 21 | // @contact.name API Support 22 | // @contact.url http://zpan.space 23 | // @contact.email saltbo@foxmail.com 24 | 25 | // @license.name GPL 3.0 26 | // @license.url https://github.com/saltbo/zpan/blob/master/LICENSE 27 | 28 | func SetupRoutes(ge *gin.Engine, repository *Repository) { 29 | ginutil.SetupSwagger(ge) 30 | 31 | apiRouter := ge.Group("/api") 32 | apiRouter.Use(authz.NewMiddleware) 33 | ginutil.SetupResource(apiRouter, 34 | repository.option, 35 | repository.file, 36 | repository.storage, 37 | repository.share, 38 | repository.token, 39 | repository.user, 40 | repository.userKey, 41 | repository.recycleBin, 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /internal/app/api/storage.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/saltbo/gopkg/ginutil" 6 | "github.com/saltbo/gopkg/jwtutil" 7 | "github.com/saltbo/zpan/internal/app/entity" 8 | "github.com/saltbo/zpan/internal/app/repo" 9 | "github.com/saltbo/zpan/internal/app/usecase/storage" 10 | "github.com/samber/lo" 11 | 12 | "github.com/saltbo/zpan/internal/pkg/bind" 13 | ) 14 | 15 | type StorageResource struct { 16 | jwtutil.JWTUtil 17 | 18 | storageRepo repo.Storage 19 | storageUc storage.Storage 20 | } 21 | 22 | func NewStorageResource(storageRepo repo.Storage, storageUc storage.Storage) *StorageResource { 23 | return &StorageResource{storageRepo: storageRepo, storageUc: storageUc} 24 | } 25 | 26 | func (rs *StorageResource) Register(router *gin.RouterGroup) { 27 | router.GET("/storages/:id", rs.find) 28 | router.GET("/storages", rs.findAll) 29 | router.POST("/storages", rs.create) 30 | router.PUT("/storages/:id", rs.update) 31 | router.DELETE("/storages/:id", rs.delete) 32 | } 33 | 34 | func (rs *StorageResource) find(c *gin.Context) { 35 | ret, err := rs.storageRepo.Find(c, ginutil.ParamInt64(c, "id")) 36 | if err != nil { 37 | ginutil.JSONServerError(c, err) 38 | return 39 | } 40 | 41 | ginutil.JSONData(c, ret) 42 | 43 | } 44 | 45 | func (rs *StorageResource) findAll(c *gin.Context) { 46 | p := new(bind.StorageQuery) 47 | if err := c.Bind(p); err != nil { 48 | ginutil.JSONBadRequest(c, err) 49 | return 50 | } 51 | 52 | list, total, err := rs.storageRepo.FindAll(c, &repo.StorageFindOptions{Limit: p.Limit, Offset: p.Offset}) 53 | if err != nil { 54 | ginutil.JSONServerError(c, err) 55 | return 56 | } 57 | 58 | lo.Map(list, func(item *entity.Storage, index int) *entity.Storage { 59 | item.SecretKey = item.SKAsterisk() 60 | return item 61 | }) 62 | 63 | ginutil.JSONList(c, list, total) 64 | } 65 | 66 | func (rs *StorageResource) create(c *gin.Context) { 67 | p := new(bind.StorageBody) 68 | if err := c.Bind(p); err != nil { 69 | ginutil.JSONBadRequest(c, err) 70 | return 71 | } 72 | 73 | if err := rs.storageUc.Create(c, p.Model()); err != nil { 74 | ginutil.JSONServerError(c, err) 75 | return 76 | } 77 | 78 | ginutil.JSON(c) 79 | } 80 | 81 | func (rs *StorageResource) update(c *gin.Context) { 82 | p := new(bind.StorageBody) 83 | if err := c.Bind(p); err != nil { 84 | ginutil.JSONBadRequest(c, err) 85 | return 86 | } 87 | 88 | if err := rs.storageRepo.Update(c, ginutil.ParamInt64(c, "id"), p.Model()); err != nil { 89 | ginutil.JSONServerError(c, err) 90 | return 91 | } 92 | 93 | ginutil.JSON(c) 94 | } 95 | 96 | func (rs *StorageResource) delete(c *gin.Context) { 97 | if err := rs.storageRepo.Delete(c, ginutil.ParamInt64(c, "id")); err != nil { 98 | ginutil.JSONServerError(c, err) 99 | return 100 | } 101 | 102 | ginutil.JSON(c) 103 | } 104 | -------------------------------------------------------------------------------- /internal/app/api/wire.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/google/wire" 4 | 5 | type Repository struct { 6 | file *FileResource 7 | recycleBin *RecycleBinResource 8 | share *ShareResource 9 | storage *StorageResource 10 | option *Option 11 | token *TokenResource 12 | user *UserResource 13 | userKey *UserKeyResource 14 | } 15 | 16 | func NewRepository(file *FileResource, recycleBin *RecycleBinResource, share *ShareResource, storage *StorageResource, option *Option, token *TokenResource, user *UserResource, userKey *UserKeyResource) *Repository { 17 | return &Repository{file: file, recycleBin: recycleBin, share: share, storage: storage, option: option, token: token, user: user, userKey: userKey} 18 | } 19 | 20 | var ProviderSet = wire.NewSet( 21 | NewStorageResource, 22 | NewFileResource, 23 | NewRecycleBinResource, 24 | NewOptionResource, 25 | NewUserResource, 26 | NewUserKeyResource, 27 | NewTokenResource, 28 | NewShareResource, 29 | NewRepository, 30 | ) 31 | -------------------------------------------------------------------------------- /internal/app/dao/dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/saltbo/zpan/internal/app/repo/query" 7 | "github.com/spf13/viper" 8 | "gorm.io/gorm" 9 | 10 | "github.com/saltbo/zpan/internal/app/model" 11 | "github.com/saltbo/zpan/internal/pkg/gormutil" 12 | ) 13 | 14 | var gdb *gorm.DB 15 | 16 | func Ready() bool { 17 | return gdb != nil 18 | } 19 | 20 | func Init(driver, dsn string) error { 21 | conf := gormutil.Config{ 22 | Driver: driver, 23 | DSN: dsn, 24 | } 25 | db, err := gormutil.New(conf) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | gdb = db.Debug() 31 | if err := gdb.AutoMigrate(model.Tables()...); err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | 38 | type DBQueryFactory struct { 39 | } 40 | 41 | func NewDBQueryFactory() *DBQueryFactory { 42 | if !viper.IsSet("installed") { 43 | return nil 44 | } 45 | 46 | if err := Init(viper.GetString("database.driver"), viper.GetString("database.dsn")); err != nil { 47 | log.Fatalln(err) 48 | } 49 | 50 | return &DBQueryFactory{} 51 | } 52 | 53 | func (D *DBQueryFactory) Q() *query.Query { 54 | return query.Use(gdb) 55 | } 56 | -------------------------------------------------------------------------------- /internal/app/dao/option.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/saltbo/zpan/internal/app/model" 5 | ) 6 | 7 | type Option struct { 8 | } 9 | 10 | func NewOption() *Option { 11 | return &Option{} 12 | } 13 | 14 | func (o *Option) Get(name string) (model.Opts, error) { 15 | ret := new(model.Option) 16 | if err := gdb.First(ret, "name=?", name).Error; err != nil { 17 | return nil, err 18 | } 19 | 20 | return ret.Opts, nil 21 | } 22 | 23 | func (o *Option) Set(name string, opts model.Opts) error { 24 | mOpt := &model.Option{Name: name} 25 | gdb.First(mOpt, "name=?", name) 26 | if opts != nil { 27 | mOpt.Opts = opts 28 | } 29 | return gdb.Save(mOpt).Error 30 | } 31 | 32 | func (o *Option) Init() error { 33 | o.Set(model.OptSite, model.DefaultSiteOpts) 34 | o.Set(model.OptEmail, model.DefaultEmailOpts) 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/app/dao/query.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Query struct { 9 | conditions []string 10 | Params []interface{} 11 | Offset int 12 | Limit int 13 | } 14 | 15 | func NewQuery() *Query { 16 | q := &Query{ 17 | conditions: make([]string, 0), 18 | Params: make([]interface{}, 0), 19 | } 20 | 21 | return q 22 | } 23 | 24 | func (q *Query) WithPage(pageNo, pageSize int64) { 25 | offset := (pageNo - 1) * pageSize 26 | q.Offset = int(offset) 27 | q.Limit = int(pageSize) 28 | } 29 | 30 | func (q *Query) WithEq(k, v interface{}) { 31 | q.conditions = append(q.conditions, fmt.Sprintf("%s=?", k)) 32 | q.Params = append(q.Params, v) 33 | } 34 | 35 | func (q *Query) WithNe(k, v interface{}) { 36 | q.conditions = append(q.conditions, fmt.Sprintf("%s!=?", k)) 37 | q.Params = append(q.Params, v) 38 | } 39 | 40 | func (q *Query) WithGt(k, v interface{}) { 41 | q.conditions = append(q.conditions, fmt.Sprintf("%s>?", k)) 42 | q.Params = append(q.Params, v) 43 | } 44 | 45 | func (q *Query) WithGte(k, v interface{}) { 46 | q.conditions = append(q.conditions, fmt.Sprintf("%s>=?", k)) 47 | q.Params = append(q.Params, v) 48 | } 49 | 50 | func (q *Query) WithLt(k, v interface{}) { 51 | q.conditions = append(q.conditions, fmt.Sprintf("%s 0 { 41 | sn = sn.Where(query.SQL(), query.Params...) 42 | } 43 | sn.Count(&total) 44 | err = sn.Offset(query.Offset).Limit(query.Limit).Preload(clause.Associations).Find(&list).Error 45 | return 46 | } 47 | 48 | func (u *UserKey) Create(uk *model.UserKey) (*model.UserKey, error) { 49 | if _, err := u.Find(uk.Uid, uk.Name); err == nil { 50 | return nil, fmt.Errorf("userKey already exist: %s", uk.Name) 51 | } 52 | 53 | if err := gdb.Create(uk).Error; err != nil { 54 | return nil, err 55 | } 56 | 57 | return uk, nil 58 | } 59 | 60 | func (u *UserKey) Update(user *model.UserKey) error { 61 | return gdb.Save(user).Error 62 | } 63 | 64 | func (u *UserKey) Delete(user *model.UserKey) error { 65 | return gdb.Delete(user).Error 66 | } 67 | -------------------------------------------------------------------------------- /internal/app/entity/matter_env.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | "github.com/saltbo/gopkg/strutil" 12 | "github.com/saltbo/gopkg/timeutil" 13 | ) 14 | 15 | type MatterEnv struct { 16 | Name string `json:"name"` 17 | Intro string `json:"intro"` 18 | Example string `json:"example"` 19 | 20 | builder func(m *Matter) string 21 | } 22 | 23 | func (env *MatterEnv) buildV(m *Matter) string { 24 | return env.builder(m) 25 | } 26 | 27 | var SupportEnvs = []MatterEnv{ 28 | {Name: "$UID", Intro: "用户ID", Example: "10001", builder: func(m *Matter) string { return strconv.FormatInt(m.Uid, 10) }}, 29 | {Name: "$UUID", Intro: "UUID", Example: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", builder: func(m *Matter) string { return uuid.New().String() }}, 30 | {Name: "$RAW_PATH", Intro: "初始上传路径", Example: "文稿/简历", builder: func(m *Matter) string { return m.Parent }}, 31 | {Name: "$RAW_NAME", Intro: "初始文件名", Example: "张三-简历", builder: func(m *Matter) string { return m.Name }}, 32 | {Name: "$RAW_EXT", Intro: "初始文件后缀", Example: "pdf", builder: func(m *Matter) string { return strings.TrimPrefix(filepath.Ext(m.Name), ".") }}, 33 | {Name: "$RAND_8KEY", Intro: "8位随机字符", Example: "mCUoR35r", builder: func(m *Matter) string { return strutil.RandomText(8) }}, 34 | {Name: "$RAND_16KEY", Intro: "16位随机字符", Example: "e1CbDUNfyVP3sScJ", builder: func(m *Matter) string { return strutil.RandomText(16) }}, 35 | {Name: "$NOW_DATE", Intro: "当前时间-日期", Example: "20210101", builder: func(m *Matter) string { return timeutil.Format(time.Now(), "YYYYMMDD") }}, 36 | {Name: "$NOW_YEAR", Intro: "当前时间-年", Example: "2021", builder: func(m *Matter) string { return strconv.Itoa(time.Now().Year()) }}, 37 | {Name: "$NOW_MONTH", Intro: "当前时间-月", Example: "01", builder: func(m *Matter) string { return strconv.Itoa(int(time.Now().Month())) }}, 38 | {Name: "$NOW_DAY", Intro: "当前时间-日", Example: "01", builder: func(m *Matter) string { return strconv.Itoa(time.Now().Day()) }}, 39 | {Name: "$NOW_HOUR", Intro: "当前时间-时", Example: "12", builder: func(m *Matter) string { return strconv.Itoa(time.Now().Hour()) }}, 40 | {Name: "$NOW_MIN", Intro: "当前时间-分", Example: "30", builder: func(m *Matter) string { return strconv.Itoa(time.Now().Minute()) }}, 41 | {Name: "$NOW_SEC", Intro: "当前时间-秒", Example: "10", builder: func(m *Matter) string { return strconv.Itoa(time.Now().Second()) }}, 42 | {Name: "$NOW_UNIX", Intro: "当前时间-时间戳", Example: "1612631185", builder: func(m *Matter) string { return fmt.Sprint(time.Now().Unix()) }}, 43 | } 44 | -------------------------------------------------------------------------------- /internal/app/entity/recycle.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type RecycleBin struct { 10 | Id int64 `json:"id"` 11 | Uid int64 `json:"uid" gorm:"not null"` 12 | Sid int64 `json:"sid" gorm:"not null"` // storage_id 13 | Mid int64 `json:"mid" gorm:"not null;default:0"` // matter_id 14 | Alias string `json:"alias" gorm:"size:16;not null"` 15 | Name string `json:"name" gorm:"not null"` 16 | Type string `json:"type" gorm:"not null"` 17 | Size int64 `json:"size" gorm:"not null"` 18 | DirType int8 `json:"dirtype" gorm:"column:dirtype;not null"` 19 | 20 | // Deprecated: 弃用 21 | Parent string `json:"parent" gorm:"not null"` 22 | 23 | // Deprecated: 弃用,不在存储具体对象 24 | Object string `json:"object" gorm:"not null"` 25 | 26 | CreatedAt time.Time `json:"created" gorm:"not null"` 27 | DeletedAt gorm.DeletedAt `json:"-"` 28 | } 29 | 30 | func (m *RecycleBin) GetID() string { 31 | return m.Alias 32 | } 33 | 34 | func (m *RecycleBin) TableName() string { 35 | return "zp_recycle" 36 | } 37 | 38 | func (m *RecycleBin) IsDir() bool { 39 | return m.DirType > 0 40 | } 41 | 42 | func (m *RecycleBin) UserAccessible(uid int64) bool { 43 | return m.Uid == uid 44 | } 45 | -------------------------------------------------------------------------------- /internal/app/entity/storage.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | const ( 10 | StorageModeNetDisk = iota + 1 11 | StorageModeOutline 12 | ) 13 | 14 | const ( 15 | StorageStatusEnabled = iota + 1 16 | StorageStatusDisabled 17 | ) 18 | 19 | type Storage struct { 20 | Id int64 `json:"id"` 21 | Mode int8 `json:"mode" gorm:"size:16;not null"` 22 | Name string `json:"name" gorm:"size:16;not null"` 23 | Title string `json:"title" gorm:"size:16;not null"` 24 | IDirs string `json:"idirs" gorm:"size:255;not null"` // internal dirs 25 | Bucket string `json:"bucket" gorm:"size:32;not null"` 26 | Provider string `json:"provider" gorm:"size:8;not null"` 27 | Endpoint string `json:"endpoint" gorm:"size:128;not null"` 28 | Region string `json:"region" gorm:"size:128;default:auto;not null"` 29 | AccessKey string `json:"access_key" gorm:"size:64;not null"` 30 | SecretKey string `json:"secret_key" gorm:"size:64;not null"` 31 | CustomHost string `json:"custom_host" gorm:"size:128;not null"` 32 | PathStyle bool `json:"path_style" gorm:"not null;default:false"` 33 | RootPath string `json:"root_path" gorm:"size:64;not null"` 34 | FilePath string `json:"file_path" gorm:"size:1024;not null"` 35 | Status int8 `json:"status" gorm:"size:1;default:1;not null"` 36 | Created time.Time `json:"created" gorm:"->;<-:create;autoCreateTime;not null"` 37 | Updated time.Time `json:"updated" gorm:"autoUpdateTime;not null"` 38 | Deleted gorm.DeletedAt `json:"-"` 39 | } 40 | 41 | func (s *Storage) GetID() int64 { 42 | return s.Id 43 | } 44 | 45 | func (s *Storage) TableName() string { 46 | return "zp_storage" 47 | } 48 | 49 | func (s *Storage) PublicRead() bool { 50 | return s.Mode == StorageModeOutline 51 | } 52 | 53 | func (s *Storage) SKAsterisk() (sk string) { 54 | for range s.SecretKey { 55 | sk += "*" 56 | } 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /internal/app/entity/user_storage.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | const ( 10 | UserStorageDefaultSize = 50 << 20 11 | UserStorageActiveSize = 1024 << 20 12 | ) 13 | 14 | type UserStorage struct { 15 | Id int64 `json:"id"` 16 | Uid int64 `json:"uid" gorm:"not null"` 17 | Max uint64 `json:"max" gorm:"not null"` 18 | Used uint64 `json:"used" gorm:"not null"` 19 | Created time.Time `json:"created" gorm:"autoCreateTime;not null"` 20 | Updated time.Time `json:"updated" gorm:"autoUpdateTime;not null"` 21 | Deleted gorm.DeletedAt `json:"-"` 22 | } 23 | 24 | func (us *UserStorage) TableName() string { 25 | return "zp_storage_quota" 26 | } 27 | 28 | func (us *UserStorage) Overflowed(addonSize int64) bool { 29 | if us.Used+uint64(addonSize) >= us.Max { 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | -------------------------------------------------------------------------------- /internal/app/model/base.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/saltbo/zpan/internal/app/entity" 4 | 5 | func Tables() []interface{} { 6 | return []interface{}{ 7 | new(Option), 8 | new(User), 9 | new(UserKey), 10 | new(UserProfile), 11 | new(entity.UserStorage), 12 | new(entity.Storage), 13 | new(entity.Matter), 14 | new(Share), 15 | new(entity.RecycleBin), 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/app/model/share.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type Share struct { 10 | Id int64 `json:"id"` 11 | Uid int64 `json:"uid" gorm:"not null"` 12 | Alias string `json:"alias" gorm:"size:16;not null"` 13 | Matter string `json:"matter" gorm:"not null"` 14 | Name string `json:"name" gorm:"not null"` 15 | Type string `json:"type" gorm:"not null"` 16 | Secret string `json:"secret,omitempty" gorm:"size:16;not null"` 17 | Protected bool `json:"protected" gorm:"-"` 18 | DownTimes int64 `json:"down_times" gorm:"not null"` 19 | ViewTimes int64 `json:"view_times" gorm:"not null"` 20 | ExpireAt time.Time `json:"expire_at" gorm:"not null"` 21 | CreateAt time.Time `json:"created" gorm:"autoCreateTime;not null"` // 这里的CreateAt应该是CreatedAt,但是先将错就错吧 22 | UpdateAt time.Time `json:"updated" gorm:"autoUpdateTime;not null"` // 这里的UpdateAt应该是UpdatedAt,但是先将错就错吧 23 | DeletedAt *time.Time `json:"-"` 24 | } 25 | 26 | func (Share) TableName() string { 27 | return "zp_share" 28 | } 29 | 30 | func (s *Share) AfterFind(*gorm.DB) (err error) { 31 | s.Protected = s.Secret != "" 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /internal/app/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "github.com/saltbo/zpan/internal/app/entity" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | const ( 13 | RoleAdmin = "admin" 14 | RoleMember = "member" 15 | RoleGuest = "guest" 16 | ) 17 | 18 | const ( 19 | StatusInactivated = iota 20 | StatusActivated 21 | StatusDisabled 22 | ) 23 | 24 | var roles = map[string]string{ 25 | RoleAdmin: "管理员", 26 | RoleMember: "注册用户", 27 | RoleGuest: "游客", 28 | } 29 | 30 | var status = map[uint8]string{ 31 | StatusInactivated: "未激活", 32 | StatusActivated: "已激活", 33 | StatusDisabled: "已禁用", 34 | } 35 | 36 | type UserCreateOption struct { 37 | Roles string 38 | Ticket string 39 | Origin string 40 | Activated bool 41 | StorageMax uint64 42 | } 43 | 44 | func NewUserCreateOption() UserCreateOption { 45 | return UserCreateOption{} 46 | } 47 | 48 | type User struct { 49 | Id int64 `json:"id"` 50 | Email string `json:"email" gorm:"size:32;unique_index;not null"` 51 | Username string `json:"username" gorm:"size:20;unique_index;not null"` 52 | Password string `json:"-" gorm:"size:32;not null"` 53 | Status uint8 `json:"-" gorm:"size:1;not null"` 54 | StatusTxt string `json:"status" gorm:"-"` 55 | Roles string `json:"-" gorm:"size:64;not null"` 56 | RoleTxt string `json:"role" gorm:"-"` 57 | Ticket string `json:"ticket" gorm:"size:6;unique_index;not null"` 58 | Profile UserProfile `json:"profile,omitempty" gorm:"foreignKey:Uid"` 59 | Storage entity.UserStorage `json:"storage,omitempty" gorm:"foreignKey:Uid"` 60 | Created time.Time `json:"created" gorm:"autoCreateTime;not null"` 61 | Updated time.Time `json:"updated" gorm:"autoUpdateTime;not null"` 62 | Deleted gorm.DeletedAt `json:"-"` 63 | 64 | Token string `json:"-" gorm:"-"` 65 | } 66 | 67 | func (User) TableName() string { 68 | return "mu_user" 69 | } 70 | 71 | func (u *User) IDString() string { 72 | return strconv.FormatInt(u.Id, 10) 73 | } 74 | 75 | func (u *User) Activated() bool { 76 | return u.Status == StatusActivated 77 | } 78 | 79 | func (u *User) RolesSplit() []string { 80 | return strings.Split(u.Roles, ",") 81 | } 82 | 83 | func (u *User) Format() *User { 84 | u.RoleTxt = roles[u.Roles] 85 | u.StatusTxt = status[u.Status] 86 | return u 87 | } 88 | -------------------------------------------------------------------------------- /internal/app/model/user_key.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/saltbo/gopkg/strutil" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | const () 13 | 14 | type UserKey struct { 15 | Id int64 `json:"id"` 16 | Uid int64 `json:"uid" gorm:"not null"` 17 | Name string `json:"name" gorm:"not null"` 18 | AccessKey string `json:"access_key" gorm:"size:32;not null"` 19 | SecretKey string `json:"secret_key" gorm:"size:64;not null"` 20 | Created time.Time `json:"created" gorm:"autoCreateTime;not null"` 21 | Updated time.Time `json:"updated" gorm:"autoUpdateTime;not null"` 22 | Deleted gorm.DeletedAt `json:"-"` 23 | } 24 | 25 | func NewUserKey(uid int64, name string) *UserKey { 26 | uk := &UserKey{ 27 | Uid: uid, 28 | Name: name, 29 | AccessKey: strutil.Md5Hex(fmt.Sprintf("%d:%d:%s", uid, time.Now().Unix(), strutil.RandomText(5))), 30 | } 31 | uk.ResetSecret() 32 | return uk 33 | } 34 | 35 | func (UserKey) TableName() string { 36 | return "zp_user_key" 37 | } 38 | 39 | func (uk *UserKey) ResetSecret() { 40 | l := strutil.Md5HexShort(strutil.RandomText(8)) 41 | r := strutil.Md5HexShort(strutil.RandomText(8)) 42 | m := strutil.Md5HexShort(l + uk.AccessKey + r) 43 | uk.SecretKey = strings.ToLower(l + m + r) 44 | } 45 | -------------------------------------------------------------------------------- /internal/app/model/user_profile.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type UserProfile struct { 10 | Id int64 `json:"id"` 11 | Uid int64 `json:"uid" gorm:"unique_index;not null"` 12 | Nickname string `json:"nickname" gorm:"size:32;not null"` 13 | Avatar string `json:"avatar" gorm:"size:255;not null"` 14 | Bio string `json:"bio" gorm:"size:255;not null"` 15 | URL string `json:"url" gorm:"size:255;not null"` 16 | Company string `json:"company" gorm:"size:32;not null"` 17 | Location string `json:"location" gorm:"size:32;not null"` 18 | Locale string `json:"locale" gorm:"not null"` 19 | Created time.Time `json:"created" gorm:"autoCreateTime;not null"` 20 | Updated time.Time `json:"updated" gorm:"autoUpdateTime;not null"` 21 | Deleted gorm.DeletedAt `json:"-"` 22 | } 23 | 24 | func (UserProfile) TableName() string { 25 | return "mu_user_profile" 26 | } 27 | -------------------------------------------------------------------------------- /internal/app/repo/matter_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "testing" 7 | 8 | "github.com/DATA-DOG/go-sqlmock" 9 | "github.com/saltbo/zpan/internal/app/entity" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMatterDBQuery_PathExist(t *testing.T) { 14 | mock, db := newMockDB(t) 15 | q := NewMatterDBQuery(db) 16 | mock.ExpectQuery("SELECT").WithArgs("to", "path/") 17 | q.PathExist(context.Background(), "/path/to/") 18 | 19 | mock.ExpectQuery("SELECT").WithArgs("a.txt", "path/to/") 20 | q.PathExist(context.Background(), "/path/to/a.txt") 21 | 22 | mock.ExpectQuery("SELECT").WithArgs("path", "") 23 | q.PathExist(context.Background(), "/path") 24 | 25 | // we make sure that all expectations were met 26 | if err := mock.ExpectationsWereMet(); err != nil { 27 | t.Errorf("there were unfulfilled expectations: %s", err) 28 | } 29 | } 30 | 31 | func TestMatterDBQuery_Update(t *testing.T) { 32 | testCases := map[string]struct { 33 | target *entity.Matter 34 | rows *sqlmock.Rows 35 | 36 | expectChildrenArgs []driver.Value // newParent, updated, oldParent 37 | expectChildrenResult driver.Result 38 | 39 | expectMainArgs []driver.Value // name, parent, updated, id 40 | expectMainResult driver.Result 41 | }{ 42 | "update name with children": { 43 | target: &entity.Matter{Id: 1, Name: "dir1-1", Parent: "dir0/", DirType: entity.DirTypeUser}, 44 | rows: sqlmock.NewRows([]string{"id", "name", "parent", "dirtype"}). 45 | AddRow(1, "dir1", "dir0", 1), 46 | 47 | expectChildrenArgs: []driver.Value{"dir0/dir1/", "dir0/dir1-1/", nowFunc(), "dir0/dir1/%"}, 48 | expectChildrenResult: sqlmock.NewResult(1, 1), 49 | 50 | expectMainArgs: []driver.Value{"dir1-1", "dir0/", nil, nowFunc(), 1}, 51 | expectMainResult: sqlmock.NewResult(1, 1), 52 | }, 53 | "update parent with children": { 54 | target: &entity.Matter{Id: 2, Name: "dir2", Parent: "dir1/", DirType: entity.DirTypeUser}, // 把dir2移动到目录dir1里 55 | rows: sqlmock.NewRows([]string{"id", "name", "parent", "dirtype"}). 56 | AddRow(2, "dir2", "", 2), 57 | 58 | expectChildrenArgs: []driver.Value{"dir2/", "dir1/dir2/", nowFunc(), "dir2/%"}, 59 | expectChildrenResult: sqlmock.NewResult(1, 1), 60 | 61 | expectMainArgs: []driver.Value{"dir2", "dir1/", nil, nowFunc(), 2}, 62 | expectMainResult: sqlmock.NewResult(1, 1), 63 | }, 64 | } 65 | 66 | for name, tc := range testCases { 67 | t.Run(name, func(t *testing.T) { 68 | mock, db := newMockDB(t) 69 | mock.ExpectQuery("SELECT").WithArgs(tc.target.Id). 70 | WillReturnRows(tc.rows) 71 | 72 | mock.ExpectBegin() 73 | mock.ExpectExec("UPDATE"). 74 | WithArgs(tc.expectChildrenArgs...). 75 | WillReturnResult(tc.expectChildrenResult) 76 | 77 | mock.ExpectExec("UPDATE"). 78 | WithArgs(tc.expectMainArgs...). 79 | WillReturnResult(tc.expectMainResult) 80 | mock.ExpectCommit() 81 | 82 | q := NewMatterDBQuery(db) 83 | ctx := context.Background() 84 | assert.NoError(t, q.Update(ctx, tc.target.Id, tc.target)) 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/app/repo/recyclebin.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | ) 8 | 9 | type RecycleBinFindOptions struct { 10 | QueryPage 11 | 12 | Sid int64 13 | Uid int64 14 | } 15 | 16 | type RecycleBin interface { 17 | Reader[*entity.RecycleBin, string, *RecycleBinFindOptions] 18 | Creator[*entity.RecycleBin] 19 | Deleter[string] 20 | } 21 | 22 | var _ RecycleBin = (*RecycleBinDBQuery)(nil) 23 | 24 | type RecycleBinDBQuery struct { 25 | DBQuery 26 | } 27 | 28 | func NewRecycleBinDBQuery(q DBQuery) *RecycleBinDBQuery { 29 | return &RecycleBinDBQuery{DBQuery: q} 30 | } 31 | 32 | func (r *RecycleBinDBQuery) Find(ctx context.Context, alias string) (*entity.RecycleBin, error) { 33 | return r.Q().RecycleBin.WithContext(ctx).Where(r.Q().RecycleBin.Alias_.Eq(alias)).First() 34 | } 35 | 36 | func (r *RecycleBinDBQuery) FindAll(ctx context.Context, opts *RecycleBinFindOptions) (rows []*entity.RecycleBin, total int64, err error) { 37 | q := r.Q().RecycleBin.WithContext(ctx).Where(r.Q().RecycleBin.Uid.Eq(opts.Uid), r.Q().RecycleBin.Sid.Eq(opts.Sid)).Order(r.Q().RecycleBin.Id.Desc()) 38 | 39 | if opts.Limit == 0 { 40 | rows, err = q.Find() 41 | return 42 | } 43 | 44 | return q.FindByPage(opts.Offset, opts.Limit) 45 | } 46 | 47 | func (r *RecycleBinDBQuery) Create(ctx context.Context, m *entity.RecycleBin) error { 48 | return r.Q().RecycleBin.WithContext(ctx).Create(m) 49 | } 50 | 51 | func (r *RecycleBinDBQuery) Delete(ctx context.Context, alias string) error { 52 | m, err := r.Find(ctx, alias) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | _, err = r.Q().RecycleBin.WithContext(ctx).Delete(m) 58 | return err 59 | } 60 | -------------------------------------------------------------------------------- /internal/app/repo/shared.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/repo/query" 7 | ) 8 | 9 | type QueryPage struct { 10 | Offset int `form:"offset"` 11 | Limit int `form:"limit,default=500"` 12 | } 13 | 14 | type Opt[p any] interface { 15 | apply(*p) 16 | } 17 | 18 | type IDType interface { 19 | int64 | string 20 | } 21 | 22 | type BasicOP[T comparable, ID IDType, O any] interface { 23 | Writer[T, ID] 24 | Reader[T, ID, O] 25 | } 26 | 27 | type Writer[T comparable, ID IDType] interface { 28 | Creator[T] 29 | Updater[T, ID] 30 | Deleter[ID] 31 | } 32 | 33 | type Reader[T comparable, ID IDType, O any] interface { 34 | Find(ctx context.Context, id ID) (T, error) 35 | FindAll(ctx context.Context, opts O) ([]T, int64, error) 36 | } 37 | 38 | type Creator[T comparable] interface { 39 | Create(ctx context.Context, entity T) error 40 | } 41 | 42 | type Updater[T comparable, ID IDType] interface { 43 | Update(ctx context.Context, id ID, entity T) error 44 | } 45 | 46 | type Deleter[ID IDType] interface { 47 | Delete(ctx context.Context, id ID) error 48 | } 49 | 50 | type DBQuery interface { 51 | Q() *query.Query 52 | } 53 | 54 | type DBQueryFactory struct { 55 | q *query.Query 56 | } 57 | 58 | func NewDBQueryFactory(q *query.Query) *DBQueryFactory { 59 | return &DBQueryFactory{q: q} 60 | } 61 | 62 | func (f *DBQueryFactory) Q() *query.Query { 63 | return f.q 64 | } 65 | -------------------------------------------------------------------------------- /internal/app/repo/shared_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/DATA-DOG/go-sqlmock" 8 | "github.com/saltbo/zpan/internal/app/repo/query" 9 | "github.com/stretchr/testify/assert" 10 | "gorm.io/driver/mysql" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | var nowFunc = func() time.Time { 15 | return time.Unix(0, 0) 16 | } 17 | 18 | func newMockDB(t *testing.T) (sqlmock.Sqlmock, DBQuery) { 19 | rdb, mock, err := sqlmock.New() 20 | assert.NoError(t, err) 21 | gdb, err := gorm.Open(mysql.New(mysql.Config{Conn: rdb, DriverName: "mysql", SkipInitializeWithVersion: true}), &gorm.Config{ 22 | NowFunc: nowFunc, 23 | }) 24 | assert.NoError(t, err) 25 | return mock, NewDBQueryFactory(query.Use(gdb.Debug())) 26 | } 27 | -------------------------------------------------------------------------------- /internal/app/repo/storage.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/saltbo/zpan/internal/app/entity" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type StorageFindOptions struct { 14 | Offset int 15 | Limit int 16 | } 17 | 18 | type Storage interface { 19 | BasicOP[*entity.Storage, int64, *StorageFindOptions] 20 | } 21 | 22 | var _ Storage = (*StorageDBQuery)(nil) 23 | 24 | type StorageDBQuery struct { 25 | DBQuery 26 | } 27 | 28 | func NewStorageDBQuery(q DBQuery) *StorageDBQuery { 29 | return &StorageDBQuery{DBQuery: q} 30 | } 31 | 32 | func (s *StorageDBQuery) Find(ctx context.Context, id int64) (*entity.Storage, error) { 33 | return s.Q().Storage.WithContext(ctx).Where(s.Q().Storage.Id.Eq(id)).First() 34 | } 35 | 36 | func (s *StorageDBQuery) FindAll(ctx context.Context, opts *StorageFindOptions) (storages []*entity.Storage, total int64, err error) { 37 | return s.Q().Storage.WithContext(ctx).FindByPage(opts.Offset, opts.Limit) 38 | } 39 | 40 | func (s *StorageDBQuery) Create(ctx context.Context, storage *entity.Storage) error { 41 | if _, err := s.Q().Storage.Where(s.Q().Storage.Name.Eq(storage.Name)).First(); !errors.Is(err, gorm.ErrRecordNotFound) { 42 | return fmt.Errorf("storage already exist") 43 | } 44 | 45 | return s.Q().Storage.WithContext(ctx).Create(storage) 46 | } 47 | 48 | func (s *StorageDBQuery) Update(ctx context.Context, id int64, storage *entity.Storage) error { 49 | existStorage, err := s.Find(ctx, id) 50 | if errors.Is(err, gorm.ErrRecordNotFound) { 51 | return fmt.Errorf("storage not found") 52 | } 53 | 54 | // 如果SK是掩码则忽略 55 | if strings.HasPrefix(storage.SecretKey, "***") { 56 | storage.SecretKey = existStorage.SecretKey 57 | } 58 | 59 | _, err = s.Q().Storage.WithContext(ctx).Where(s.Q().Storage.Id.Eq(id)).Updates(storage) 60 | return err 61 | } 62 | 63 | func (s *StorageDBQuery) Delete(ctx context.Context, id int64) error { 64 | storage := new(entity.Storage) 65 | if _, err := s.Find(ctx, id); errors.Is(err, gorm.ErrRecordNotFound) { 66 | return fmt.Errorf("storage not exist") 67 | } 68 | 69 | _, err := s.Q().Storage.WithContext(ctx).Delete(storage) 70 | return err 71 | } 72 | -------------------------------------------------------------------------------- /internal/app/repo/user.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type User interface { 11 | GetUserStorage(ctx context.Context, uid int64) (*entity.UserStorage, error) 12 | UserStorageUsedIncr(ctx context.Context, matter *entity.Matter) error 13 | UserStorageUsedDecr(ctx context.Context, matter *entity.Matter) error 14 | } 15 | 16 | var _ User = (*UserDBQuery)(nil) 17 | 18 | type UserDBQuery struct { 19 | DBQuery 20 | } 21 | 22 | func NewUserDBQuery(q DBQuery) *UserDBQuery { 23 | return &UserDBQuery{DBQuery: q} 24 | } 25 | 26 | func (u *UserDBQuery) GetUserStorage(ctx context.Context, uid int64) (*entity.UserStorage, error) { 27 | return u.Q().UserStorage.WithContext(ctx).Where(u.Q().UserStorage.Uid.Eq(uid)).First() 28 | } 29 | 30 | func (u *UserDBQuery) UserStorageUsedIncr(ctx context.Context, matter *entity.Matter) error { 31 | q := u.Q().UserStorage.WithContext(ctx).Where(u.Q().UserStorage.Uid.Eq(matter.Uid)) 32 | _, err := q.Update(u.Q().UserStorage.Used, gorm.Expr("used+?", matter.Size)) 33 | return err 34 | } 35 | 36 | func (u *UserDBQuery) UserStorageUsedDecr(ctx context.Context, matter *entity.Matter) error { 37 | q := u.Q().UserStorage.WithContext(ctx).Where(u.Q().UserStorage.Uid.Eq(matter.Uid)) 38 | userStorage, err := q.First() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | used := uint64(matter.Size) 44 | if used > userStorage.Used { 45 | used = userStorage.Used // 使用量不能变成负数 46 | } 47 | 48 | _, err = q.Update(u.Q().UserStorage.Used, gorm.Expr("used-?", used)) 49 | return err 50 | } 51 | -------------------------------------------------------------------------------- /internal/app/repo/wire.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/google/wire" 4 | 5 | type Repository struct { 6 | Storage Storage 7 | Matter Matter 8 | RecycleBin RecycleBin 9 | } 10 | 11 | func NewRepository(storage Storage, matter Matter, recycleBin RecycleBin) *Repository { 12 | return &Repository{Storage: storage, Matter: matter, RecycleBin: recycleBin} 13 | } 14 | 15 | var ProviderSet = wire.NewSet( 16 | NewUserDBQuery, 17 | wire.Bind(new(User), new(*UserDBQuery)), 18 | 19 | NewStorageDBQuery, 20 | wire.Bind(new(Storage), new(*StorageDBQuery)), 21 | 22 | NewMatterDBQuery, 23 | wire.Bind(new(Matter), new(*MatterDBQuery)), 24 | 25 | NewRecycleBinDBQuery, 26 | wire.Bind(new(RecycleBin), new(*RecycleBinDBQuery)), 27 | 28 | NewRepository, 29 | ) 30 | -------------------------------------------------------------------------------- /internal/app/server.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/saltbo/gopkg/ginutil" 8 | "github.com/saltbo/zpan/internal/app/api" 9 | "github.com/saltbo/zpan/internal/app/repo" 10 | "github.com/saltbo/zpan/internal/app/usecase" 11 | "github.com/saltbo/zpan/web" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | type Server struct { 16 | uc *usecase.Repository 17 | rp *repo.Repository 18 | ap *api.Repository 19 | } 20 | 21 | func NewServer(uc *usecase.Repository, rp *repo.Repository, ap *api.Repository) *Server { 22 | return &Server{uc: uc, rp: rp, ap: ap} 23 | } 24 | 25 | func (s *Server) Run() error { 26 | // gin.SetMode(gin.ReleaseMode) 27 | ge := gin.Default() 28 | api.SetupRoutes(ge, s.ap) 29 | web.SetupRoutes(ge) 30 | ginutil.Startup(ge, fmt.Sprintf(":%d", viper.GetInt("port"))) 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/app/service/option.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/saltbo/zpan/internal/app/dao" 7 | "github.com/saltbo/zpan/internal/app/model" 8 | ) 9 | 10 | type BootFunc func(opts model.Opts) error 11 | 12 | var optBoots = map[string]BootFunc{} 13 | 14 | func OptRegister(name string, bf BootFunc) { 15 | optBoots[name] = bf 16 | if !dao.Ready() { 17 | return // 如果数据库还没装好则先跳过 18 | } 19 | 20 | dOpt := dao.NewOption() 21 | opts, err := dOpt.Get(name) 22 | if err != nil { 23 | dOpt.Set(name, map[string]interface{}{}) 24 | return 25 | } 26 | 27 | // 检查boot参数是否存在 28 | // 如果不存在则直接跳过 29 | if len(opts) == 0 { 30 | log.Printf("WARN: skip boot for the component %s", name) 31 | return 32 | } 33 | 34 | // 如果存在则执行一次BootFunc 35 | if err := bf(opts); err != nil { 36 | log.Printf("ERR: opt-%s boot failed: %s\n", name, err) 37 | return 38 | } 39 | } 40 | 41 | type Option struct { 42 | dOption *dao.Option 43 | } 44 | 45 | func NewOption() *Option { 46 | return &Option{ 47 | dOption: dao.NewOption(), 48 | } 49 | } 50 | 51 | func (o *Option) Update(name string, p model.Opts) error { 52 | if boot, ok := optBoots[name]; ok { 53 | if err := boot(p); err != nil { 54 | return err 55 | } 56 | } 57 | 58 | return o.dOption.Set(name, p) 59 | } 60 | -------------------------------------------------------------------------------- /internal/app/service/token.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/golang-jwt/jwt" 9 | "github.com/saltbo/gopkg/jwtutil" 10 | ) 11 | 12 | type Token struct { 13 | } 14 | 15 | func NewToken() *Token { 16 | jwtutil.Init("123") 17 | return &Token{} 18 | } 19 | 20 | func (s *Token) Create(uid string, ttl int, roles ...string) (string, error) { 21 | return jwtutil.Issue(NewRoleClaims(uid, ttl, roles)) 22 | } 23 | 24 | func (s *Token) Verify(tokenStr string) (*RoleClaims, error) { 25 | token, err := jwtutil.Verify(tokenStr, &RoleClaims{}) 26 | if err != nil { 27 | return nil, fmt.Errorf("token valid failed: %s", err) 28 | } 29 | 30 | return token.Claims.(*RoleClaims), nil 31 | } 32 | 33 | type RoleClaims struct { 34 | jwt.StandardClaims 35 | 36 | Roles []string `json:"roles"` 37 | } 38 | 39 | func NewRoleClaims(subject string, ttl int, roles []string) *RoleClaims { 40 | timeNow := time.Now() 41 | return &RoleClaims{ 42 | StandardClaims: jwt.StandardClaims{ 43 | Issuer: "zplat", 44 | Audience: "zplatUsers", 45 | ExpiresAt: timeNow.Add(time.Duration(ttl) * time.Second).Unix(), 46 | IssuedAt: timeNow.Unix(), 47 | NotBefore: timeNow.Unix(), 48 | Subject: subject, 49 | }, 50 | Roles: roles, 51 | } 52 | } 53 | 54 | func (rc *RoleClaims) Uid() int64 { 55 | uid, _ := strconv.ParseInt(rc.Subject, 10, 64) 56 | return uid 57 | } 58 | -------------------------------------------------------------------------------- /internal/app/service/user_key.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "log" 7 | "strings" 8 | 9 | "github.com/go-oauth2/oauth2/v4" 10 | "github.com/go-oauth2/oauth2/v4/models" 11 | "github.com/go-oauth2/oauth2/v4/store" 12 | "github.com/google/uuid" 13 | "github.com/saltbo/zpan/internal/app/dao" 14 | "github.com/saltbo/zpan/internal/app/model" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | var cs = store.NewClientStore() 19 | 20 | type UserKey struct { 21 | dUserKey *dao.UserKey 22 | 23 | sToken *Token 24 | } 25 | 26 | func NewUserKey() *UserKey { 27 | return &UserKey{ 28 | dUserKey: dao.NewUserKey(), 29 | 30 | sToken: NewToken(), 31 | } 32 | } 33 | 34 | func (uk *UserKey) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (access, refresh string, err error) { 35 | muk, err := uk.dUserKey.FindByClientID(data.Client.GetID()) 36 | if err != nil { 37 | return "", "", err 38 | } 39 | 40 | user, err := dao.NewUser().Find(muk.Uid) 41 | if err != nil { 42 | return "", "", err 43 | } 44 | 45 | ttl := data.TokenInfo.GetAccessCreateAt().Add(data.TokenInfo.GetAccessExpiresIn()).Unix() 46 | access, err = uk.sToken.Create(user.IDString(), int(ttl), user.Roles) 47 | if err != nil { 48 | return 49 | } 50 | 51 | if isGenRefresh { 52 | t := uuid.NewSHA1(uuid.Must(uuid.NewRandom()), []byte(access)).String() 53 | refresh = base64.URLEncoding.EncodeToString([]byte(t)) 54 | refresh = strings.ToUpper(strings.TrimRight(refresh, "=")) 55 | } 56 | return 57 | } 58 | 59 | func (uk *UserKey) ClientStore() *store.ClientStore { 60 | return cs 61 | } 62 | 63 | func (uk *UserKey) Create(muk *model.UserKey) error { 64 | if _, err := uk.dUserKey.Create(muk); err != nil { 65 | return err 66 | } 67 | 68 | return uk.ClientStore().Set(muk.AccessKey, &models.Client{ID: muk.AccessKey, Secret: muk.SecretKey}) 69 | } 70 | 71 | func (uk *UserKey) ResetSecret(muk *model.UserKey) error { 72 | muk.ResetSecret() 73 | if err := uk.dUserKey.Update(muk); err != nil { 74 | return err 75 | } 76 | 77 | return uk.ClientStore().Set(muk.AccessKey, &models.Client{ID: muk.AccessKey, Secret: muk.SecretKey}) 78 | } 79 | 80 | func (uk *UserKey) LoadExistClient() { 81 | if !viper.IsSet("installed") { 82 | return 83 | } 84 | 85 | list, _, err := uk.dUserKey.FindAll(dao.NewQuery()) 86 | if err != nil { 87 | log.Println(err) 88 | return 89 | } 90 | 91 | for _, muk := range list { 92 | cli := &models.Client{ID: muk.AccessKey, Secret: muk.SecretKey} 93 | if err := uk.ClientStore().Set(muk.AccessKey, cli); err != nil { 94 | log.Println(err) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/app/usecase/authz/authz.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/open-policy-agent/opa/rego" 10 | "github.com/saltbo/zpan/internal/pkg/authed" 11 | ) 12 | 13 | type Input struct { 14 | Uid int64 `json:"uid"` 15 | Path string `json:"path"` 16 | PathParams []gin.Param `json:"path_params"` 17 | Resource any `json:"resource"` 18 | } 19 | 20 | //go:embed authz.rego 21 | var module string 22 | 23 | func NewMiddleware(c *gin.Context) { 24 | bw := NewWriter(c.Writer) 25 | c.Writer = bw 26 | c.Next() 27 | 28 | input := &Input{ 29 | Uid: authed.UidGet(c), 30 | Path: c.FullPath(), 31 | PathParams: c.Params, 32 | Resource: bw.extractResource(), 33 | } 34 | 35 | if rs, err := Decision(c, input); err != nil { 36 | _ = c.AbortWithError(http.StatusInternalServerError, err) 37 | return 38 | } else if !rs.Allowed() { 39 | c.AbortWithStatus(http.StatusForbidden) 40 | return 41 | } 42 | 43 | bw.WriteNow() 44 | } 45 | 46 | func Decision(ctx context.Context, input *Input) (rego.ResultSet, error) { 47 | r := rego.New( 48 | rego.Query("data.authz.allow"), 49 | rego.Module("./authz.rego", module), 50 | ) 51 | 52 | query, err := r.PrepareForEval(ctx) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return query.Eval(ctx, rego.EvalInput(input)) 58 | } 59 | -------------------------------------------------------------------------------- /internal/app/usecase/authz/authz.rego: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import future.keywords.contains 4 | import future.keywords.if 5 | import future.keywords.in 6 | 7 | default allow := true 8 | 9 | allow := false { 10 | input.resource.data.uid != input.uid 11 | } -------------------------------------------------------------------------------- /internal/app/usecase/authz/writer.go: -------------------------------------------------------------------------------- 1 | package authz 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "log" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type Writer struct { 12 | gin.ResponseWriter 13 | 14 | Buf bytes.Buffer 15 | } 16 | 17 | func NewWriter(rw gin.ResponseWriter) *Writer { 18 | return &Writer{ 19 | ResponseWriter: rw, 20 | } 21 | } 22 | 23 | func (w *Writer) Write(p []byte) (n int, err error) { 24 | return w.Buf.Write(p) 25 | } 26 | 27 | func (w *Writer) extractResource() any { 28 | buf := make([]byte, w.Buf.Len()) 29 | copy(buf, w.Buf.Bytes()) 30 | var resource interface{} 31 | dec := json.NewDecoder(bytes.NewReader(buf)) 32 | dec.UseNumber() 33 | if err := dec.Decode(&resource); err != nil { 34 | log.Fatal(err) 35 | } 36 | return resource 37 | } 38 | 39 | func (w *Writer) WriteNow() { 40 | _, _ = w.ResponseWriter.Write(w.Buf.Bytes()) 41 | } 42 | -------------------------------------------------------------------------------- /internal/app/usecase/storage/cloud_storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | "github.com/saltbo/zpan/internal/app/repo" 8 | "github.com/saltbo/zpan/internal/pkg/provider" 9 | ) 10 | 11 | var _ Storage = (*CloudStorage)(nil) 12 | 13 | type CloudStorage struct { 14 | storageRepo repo.Storage 15 | 16 | providerConstructor provider.Constructor 17 | } 18 | 19 | func NewCloudStorage(storageRepo repo.Storage) *CloudStorage { 20 | return &CloudStorage{storageRepo: storageRepo, providerConstructor: provider.New} 21 | } 22 | 23 | func NewCloudStorageWithProviderConstructor(storageRepo repo.Storage, providerConstructor provider.Constructor) *CloudStorage { 24 | return &CloudStorage{storageRepo: storageRepo, providerConstructor: providerConstructor} 25 | } 26 | 27 | func (s *CloudStorage) Create(ctx context.Context, storage *entity.Storage) error { 28 | p, err := s.providerConstructor(s.buildConfig(storage)) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | if err := p.SetupCORS(); err != nil { 34 | return err 35 | } 36 | 37 | return s.storageRepo.Create(ctx, storage) 38 | } 39 | 40 | func (s *CloudStorage) Get(ctx context.Context, sid int64) (*entity.Storage, error) { 41 | return s.storageRepo.Find(ctx, sid) 42 | } 43 | 44 | func (s *CloudStorage) GetProvider(ctx context.Context, sid int64) (provider.Provider, error) { 45 | storage, err := s.storageRepo.Find(ctx, sid) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return s.GetProviderByStorage(storage) 51 | } 52 | 53 | func (s *CloudStorage) GetProviderByStorage(storage *entity.Storage) (provider.Provider, error) { 54 | return s.providerConstructor(s.buildConfig(storage)) 55 | } 56 | 57 | func (s *CloudStorage) buildConfig(storage *entity.Storage) *provider.Config { 58 | return &provider.Config{ 59 | Provider: storage.Provider, 60 | Bucket: storage.Bucket, 61 | Endpoint: storage.Endpoint, 62 | Region: storage.Region, 63 | CustomHost: storage.CustomHost, 64 | AccessKey: storage.AccessKey, 65 | AccessSecret: storage.SecretKey, 66 | PathStyle: storage.PathStyle, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/app/usecase/storage/cloud_storage_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/saltbo/zpan/internal/app/entity" 8 | "github.com/saltbo/zpan/internal/mock" 9 | "github.com/saltbo/zpan/internal/pkg/provider" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var ( 14 | testStorage = &entity.Storage{ 15 | Id: 9527, 16 | Name: "test", 17 | Title: "TEST", 18 | } 19 | ) 20 | 21 | func TestCloudStorage_Create(t *testing.T) { 22 | ctx := context.Background() 23 | s := NewCloudStorageWithProviderConstructor(mock.NewStorage(), provider.NewMockProvider) 24 | assert.NoError(t, s.Create(context.Background(), testStorage)) 25 | ss, err := s.Get(ctx, testStorage.Id) 26 | assert.NoError(t, err) 27 | assert.Equal(t, testStorage, ss) 28 | } 29 | 30 | func TestCloudStorage_GetProvider(t *testing.T) { 31 | ctx := context.Background() 32 | s := NewCloudStorageWithProviderConstructor(mock.NewStorage(), provider.NewMockProvider) 33 | assert.NoError(t, s.Create(context.Background(), testStorage)) 34 | 35 | pp, err := s.GetProvider(ctx, testStorage.Id) 36 | assert.NoError(t, err) 37 | mpp, err := provider.NewMockProvider(s.buildConfig(testStorage)) 38 | assert.NoError(t, err) 39 | assert.Equal(t, mpp, pp) 40 | } 41 | -------------------------------------------------------------------------------- /internal/app/usecase/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | "github.com/saltbo/zpan/internal/pkg/provider" 8 | ) 9 | 10 | type Storage interface { 11 | Create(ctx context.Context, storage *entity.Storage) error 12 | Get(ctx context.Context, sid int64) (*entity.Storage, error) 13 | GetProvider(ctx context.Context, id int64) (provider.Provider, error) 14 | } 15 | -------------------------------------------------------------------------------- /internal/app/usecase/uploader/cloud_uploader.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | "github.com/saltbo/zpan/internal/app/repo" 8 | "github.com/saltbo/zpan/internal/app/usecase/storage" 9 | ) 10 | 11 | var _ Uploader = (*CloudUploader)(nil) 12 | 13 | type CloudUploader struct { 14 | storage storage.Storage 15 | matterRepo repo.Matter 16 | } 17 | 18 | func NewCloudUploader(storage storage.Storage, matterRepo repo.Matter) *CloudUploader { 19 | return &CloudUploader{storage: storage, matterRepo: matterRepo} 20 | } 21 | 22 | func (u *CloudUploader) CreateUploadURL(ctx context.Context, m *entity.Matter) error { 23 | provider, err := u.storage.GetProvider(ctx, m.Sid) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | s, err := u.storage.Get(ctx, m.Sid) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | m.BuildObject(s.RootPath, s.FilePath) 34 | urlStr, header, err := provider.SignedPutURL(m.Object, m.Type, m.Size, s.PublicRead()) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | m.Uploader["upURL"] = urlStr 40 | m.Uploader["upHeaders"] = header 41 | return nil 42 | } 43 | 44 | func (u *CloudUploader) CreateVisitURL(ctx context.Context, m *entity.Matter) error { 45 | provider, err := u.storage.GetProvider(ctx, m.Sid) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | s, err := u.storage.Get(ctx, m.Sid) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if s.PublicRead() { 56 | m.URL = provider.PublicURL(m.Object) 57 | return nil 58 | } 59 | 60 | link, err := provider.SignedGetURL(m.Object, m.Name) 61 | m.URL = link 62 | return err 63 | } 64 | 65 | func (u *CloudUploader) UploadDone(ctx context.Context, m *entity.Matter) error { 66 | provider, err := u.storage.GetProvider(ctx, m.Sid) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if _, err := provider.Head(m.Object); err != nil { 72 | return err 73 | } 74 | 75 | m.SetUploadedAt() 76 | return u.matterRepo.Update(ctx, m.Id, m) 77 | } 78 | -------------------------------------------------------------------------------- /internal/app/usecase/uploader/cloud_uploader_test.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | -------------------------------------------------------------------------------- /internal/app/usecase/uploader/fake_uploader.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | ) 8 | 9 | type FakeUploader struct { 10 | CreateUploadURLFn func(ctx context.Context, m *entity.Matter) error 11 | } 12 | 13 | func (f *FakeUploader) CreateUploadURL(ctx context.Context, m *entity.Matter) error { 14 | return f.CreateUploadURLFn(ctx, m) 15 | } 16 | 17 | func (f *FakeUploader) CreateVisitURL(ctx context.Context, m *entity.Matter) error { 18 | return nil 19 | } 20 | 21 | func (f *FakeUploader) UploadDone(ctx context.Context, m *entity.Matter) error { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/app/usecase/uploader/uploader.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | ) 8 | 9 | type Uploader interface { 10 | CreateUploadURL(ctx context.Context, m *entity.Matter) error 11 | CreateVisitURL(ctx context.Context, m *entity.Matter) error 12 | UploadDone(ctx context.Context, m *entity.Matter) error 13 | } 14 | -------------------------------------------------------------------------------- /internal/app/usecase/vfs/interfaces.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | "github.com/saltbo/zpan/internal/app/repo" 8 | ) 9 | 10 | type VirtualFs interface { 11 | Create(ctx context.Context, m *entity.Matter) error 12 | List(ctx context.Context, option *repo.MatterListOption) ([]*entity.Matter, int64, error) 13 | Get(ctx context.Context, alias string) (*entity.Matter, error) 14 | Rename(ctx context.Context, alias string, newName string) error 15 | Move(ctx context.Context, alias string, to string) error 16 | Copy(ctx context.Context, alias string, to string) (*entity.Matter, error) 17 | Delete(ctx context.Context, alias string) error 18 | } 19 | 20 | type RecycleBinFs interface { 21 | Recovery(ctx context.Context, alias string) error 22 | Delete(ctx context.Context, alias string) error 23 | Clean(ctx context.Context, sid, uid int64) error 24 | } 25 | -------------------------------------------------------------------------------- /internal/app/usecase/vfs/recyclebin.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/repo" 7 | "github.com/saltbo/zpan/internal/app/usecase/storage" 8 | ) 9 | 10 | var _ RecycleBinFs = (*RecycleBin)(nil) 11 | 12 | type RecycleBin struct { 13 | recycleRepo repo.RecycleBin 14 | matterRepo repo.Matter 15 | userRepo repo.User 16 | storage storage.Storage 17 | } 18 | 19 | func NewRecycleBin(recycleRepo repo.RecycleBin, matterRepo repo.Matter, userRepo repo.User, storage storage.Storage) *RecycleBin { 20 | return &RecycleBin{recycleRepo: recycleRepo, matterRepo: matterRepo, userRepo: userRepo, storage: storage} 21 | } 22 | 23 | func (rb *RecycleBin) Recovery(ctx context.Context, alias string) error { 24 | rbv, err := rb.recycleRepo.Find(ctx, alias) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if err := rb.matterRepo.Recovery(ctx, rbv.Mid); err != nil { 30 | return err 31 | } 32 | 33 | return rb.recycleRepo.Delete(ctx, alias) 34 | } 35 | 36 | func (rb *RecycleBin) Delete(ctx context.Context, alias string) error { 37 | m, err := rb.recycleRepo.Find(ctx, alias) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | matter, err := rb.matterRepo.FindWith(ctx, &repo.MatterFindWithOption{Id: m.Mid, Deleted: true}) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | provider, err := rb.storage.GetProvider(ctx, matter.Sid) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | objects, _ := rb.matterRepo.GetObjects(ctx, matter.Id) 53 | if len(objects) != 0 { 54 | if err := provider.ObjectsDelete(objects); err != nil { 55 | return err 56 | } 57 | } 58 | 59 | defer rb.userRepo.UserStorageUsedDecr(ctx, matter) 60 | return rb.recycleRepo.Delete(ctx, alias) 61 | } 62 | 63 | func (rb *RecycleBin) Clean(ctx context.Context, sid, uid int64) error { 64 | rbs, _, err := rb.recycleRepo.FindAll(ctx, &repo.RecycleBinFindOptions{Sid: sid, Uid: uid}) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | for _, rbMatter := range rbs { 70 | if err := rb.Delete(ctx, rbMatter.Alias); err != nil { 71 | return err 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/app/usecase/vfs/vfs_jobs.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/robfig/cron" 9 | "github.com/saltbo/zpan/internal/app/entity" 10 | "github.com/saltbo/zpan/internal/app/repo" 11 | ) 12 | 13 | func (v *Vfs) matterCreatedEventHandler(matter *entity.Matter) error { 14 | c := cron.New() 15 | c.Start() 16 | return c.AddFunc("@every 10s", func() { 17 | ctx := context.Background() 18 | if err := v.uploader.UploadDone(ctx, matter); err != nil { 19 | return 20 | } 21 | 22 | _ = v.userRepo.UserStorageUsedIncr(ctx, matter) 23 | c.Stop() 24 | }) 25 | } 26 | 27 | func (v *Vfs) matterDeletedEventHandler(matter *entity.Matter) error { 28 | return nil 29 | } 30 | 31 | func (v *Vfs) cleanExpiredMatters() { 32 | ctx := context.Background() 33 | matters, _, err := v.matterRepo.FindAll(ctx, &repo.MatterListOption{Draft: true}) 34 | if err != nil { 35 | log.Printf("error getting the files of not uploaded: %s", err) 36 | return 37 | } 38 | 39 | for _, matter := range matters { 40 | if time.Since(matter.CreatedAt) < time.Hour*24 { 41 | continue 42 | } 43 | 44 | if err := v.matterRepo.Delete(ctx, matter.Id); err != nil { 45 | log.Printf("error deleting the file %s: %s", matter.FullPath(), err) 46 | return 47 | } 48 | 49 | log.Printf("deleted the file: %s", matter.FullPath()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/app/usecase/vfs/worker.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "github.com/saltbo/zpan/internal/app/entity" 5 | "github.com/sourcegraph/conc/pool" 6 | ) 7 | 8 | const ( 9 | EventActionCreated EventAction = "Created" 10 | EventActionDeleted EventAction = "Deleted" 11 | ) 12 | 13 | type ( 14 | EventAction string 15 | EventHandler func(matter *entity.Matter) error 16 | ) 17 | 18 | type Event struct { 19 | Action EventAction 20 | Matter *entity.Matter 21 | } 22 | 23 | type EventWorker struct { 24 | eventChan chan Event 25 | eventReg map[EventAction]EventHandler 26 | } 27 | 28 | func NewWorker() *EventWorker { 29 | return &EventWorker{ 30 | eventChan: make(chan Event), 31 | eventReg: make(map[EventAction]EventHandler), 32 | } 33 | } 34 | 35 | func (w *EventWorker) Run() { 36 | p := pool.New().WithMaxGoroutines(10) 37 | for elem := range w.eventChan { 38 | eventHandle := w.eventReg[elem.Action] 39 | p.Go(func() { 40 | if err := eventHandle(elem.Matter); err != nil { 41 | return 42 | } 43 | }) 44 | } 45 | p.Wait() 46 | } 47 | 48 | func (w *EventWorker) registerEventHandler(action EventAction, h EventHandler) { 49 | w.eventReg[action] = h 50 | } 51 | 52 | func (w *EventWorker) sendEvent(action EventAction, m *entity.Matter) { 53 | w.eventChan <- Event{Action: action, Matter: m} 54 | } 55 | -------------------------------------------------------------------------------- /internal/app/usecase/wire.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "github.com/google/wire" 5 | "github.com/saltbo/zpan/internal/app/usecase/storage" 6 | "github.com/saltbo/zpan/internal/app/usecase/uploader" 7 | "github.com/saltbo/zpan/internal/app/usecase/vfs" 8 | ) 9 | 10 | type Repository struct { 11 | Storage storage.Storage 12 | Uploader uploader.Uploader 13 | VFS vfs.VirtualFs 14 | } 15 | 16 | func NewRepository(storage storage.Storage, uploader uploader.Uploader, VFS vfs.VirtualFs) *Repository { 17 | return &Repository{Storage: storage, Uploader: uploader, VFS: VFS} 18 | } 19 | 20 | var ProviderSet = wire.NewSet( 21 | storage.NewCloudStorage, 22 | uploader.NewCloudUploader, 23 | vfs.NewVfs, 24 | vfs.NewRecycleBin, 25 | 26 | wire.Bind(new(storage.Storage), new(*storage.CloudStorage)), 27 | wire.Bind(new(uploader.Uploader), new(*uploader.CloudUploader)), 28 | wire.Bind(new(vfs.VirtualFs), new(*vfs.Vfs)), 29 | wire.Bind(new(vfs.RecycleBinFs), new(*vfs.RecycleBin)), 30 | NewRepository, 31 | ) 32 | -------------------------------------------------------------------------------- /internal/app/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package app 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/saltbo/zpan/internal/app/api" 9 | "github.com/saltbo/zpan/internal/app/dao" 10 | "github.com/saltbo/zpan/internal/app/repo" 11 | "github.com/saltbo/zpan/internal/app/usecase" 12 | ) 13 | 14 | func InitializeServer() *Server { 15 | wire.Build( 16 | dao.NewDBQueryFactory, 17 | wire.Bind(new(repo.DBQuery), new(*dao.DBQueryFactory)), 18 | 19 | repo.ProviderSet, 20 | usecase.ProviderSet, 21 | api.ProviderSet, 22 | NewServer, 23 | ) 24 | return &Server{} 25 | } 26 | -------------------------------------------------------------------------------- /internal/app/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package app 8 | 9 | import ( 10 | "github.com/saltbo/zpan/internal/app/api" 11 | "github.com/saltbo/zpan/internal/app/dao" 12 | "github.com/saltbo/zpan/internal/app/repo" 13 | "github.com/saltbo/zpan/internal/app/usecase" 14 | "github.com/saltbo/zpan/internal/app/usecase/storage" 15 | "github.com/saltbo/zpan/internal/app/usecase/uploader" 16 | "github.com/saltbo/zpan/internal/app/usecase/vfs" 17 | ) 18 | 19 | // Injectors from wire.go: 20 | 21 | func InitializeServer() *Server { 22 | dbQueryFactory := dao.NewDBQueryFactory() 23 | storageDBQuery := repo.NewStorageDBQuery(dbQueryFactory) 24 | cloudStorage := storage.NewCloudStorage(storageDBQuery) 25 | matterDBQuery := repo.NewMatterDBQuery(dbQueryFactory) 26 | cloudUploader := uploader.NewCloudUploader(cloudStorage, matterDBQuery) 27 | recycleBinDBQuery := repo.NewRecycleBinDBQuery(dbQueryFactory) 28 | userDBQuery := repo.NewUserDBQuery(dbQueryFactory) 29 | vfsVfs := vfs.NewVfs(matterDBQuery, recycleBinDBQuery, userDBQuery, cloudUploader) 30 | repository := usecase.NewRepository(cloudStorage, cloudUploader, vfsVfs) 31 | repoRepository := repo.NewRepository(storageDBQuery, matterDBQuery, recycleBinDBQuery) 32 | fileResource := api.NewFileResource(vfsVfs, cloudUploader) 33 | recycleBin := vfs.NewRecycleBin(recycleBinDBQuery, matterDBQuery, userDBQuery, cloudStorage) 34 | recycleBinResource := api.NewRecycleBinResource(recycleBinDBQuery, recycleBin) 35 | shareResource := api.NewShareResource(matterDBQuery, vfsVfs) 36 | storageResource := api.NewStorageResource(storageDBQuery, cloudStorage) 37 | option := api.NewOptionResource() 38 | tokenResource := api.NewTokenResource() 39 | userResource := api.NewUserResource() 40 | userKeyResource := api.NewUserKeyResource() 41 | apiRepository := api.NewRepository(fileResource, recycleBinResource, shareResource, storageResource, option, tokenResource, userResource, userKeyResource) 42 | server := NewServer(repository, repoRepository, apiRepository) 43 | return server 44 | } 45 | -------------------------------------------------------------------------------- /internal/mock/matter.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/saltbo/zpan/internal/app/entity" 8 | "github.com/saltbo/zpan/internal/app/repo" 9 | "github.com/samber/lo" 10 | ) 11 | 12 | var _ repo.Matter = (*Matter)(nil) 13 | 14 | type Matter struct { 15 | mockStore[*entity.Matter, *repo.MatterListOption, int64] 16 | } 17 | 18 | func NewMatter() *Matter { 19 | return &Matter{} 20 | } 21 | 22 | func (mk *Matter) FindWith(ctx context.Context, opt *repo.MatterFindWithOption) (*entity.Matter, error) { 23 | matter, ok := lo.Find(mk.store, func(item *entity.Matter) bool { 24 | var conds []bool 25 | if opt.Id != 0 { 26 | conds = append(conds, opt.Id == item.Id) 27 | } 28 | if opt.Alias != "" { 29 | conds = append(conds, opt.Alias == item.Alias) 30 | } 31 | 32 | for _, cond := range conds { 33 | if !cond { 34 | return false 35 | } 36 | } 37 | return true 38 | }) 39 | if !ok { 40 | return nil, fmt.Errorf("not found with: %v", opt) 41 | } 42 | 43 | return matter, nil 44 | } 45 | 46 | func (mk *Matter) FindByAlias(ctx context.Context, alias string) (*entity.Matter, error) { 47 | matter, ok := lo.Find(mk.store, func(item *entity.Matter) bool { 48 | return item.Alias == alias 49 | }) 50 | if !ok { 51 | return nil, fmt.Errorf("not found: %v", alias) 52 | } 53 | 54 | return matter, nil 55 | } 56 | 57 | func (mk *Matter) PathExist(ctx context.Context, filepath string) bool { 58 | _, ok := lo.Find(mk.store, func(item *entity.Matter) bool { return item.FullPath() == filepath }) 59 | return ok 60 | } 61 | 62 | func (mk *Matter) Copy(ctx context.Context, id int64, to string) (*entity.Matter, error) { 63 | matter, err := mk.Find(ctx, id) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | newMatter := matter.Clone() 69 | newMatter.Parent = to 70 | mk.store = append(mk.store, newMatter) 71 | return newMatter, nil 72 | } 73 | 74 | func (mk *Matter) Recovery(ctx context.Context, id int64) error { 75 | return nil 76 | } 77 | 78 | func (mk *Matter) GetObjects(ctx context.Context, id int64) ([]string, error) { 79 | return []string{}, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/mock/mock.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/saltbo/zpan/internal/app/repo" 8 | "github.com/samber/lo" 9 | ) 10 | 11 | type Entity[ID repo.IDType] interface { 12 | comparable 13 | GetID() ID 14 | } 15 | 16 | type mockStore[T Entity[ID], O any, ID repo.IDType] struct { 17 | store []T 18 | } 19 | 20 | func (ms *mockStore[T, O, ID]) Create(ctx context.Context, t T) error { 21 | ms.store = append(ms.store, t) 22 | return nil 23 | } 24 | 25 | func (ms *mockStore[T, O, ID]) Find(ctx context.Context, id ID) (T, error) { 26 | v, ok := lo.Find(ms.store, func(item T) bool { 27 | return item.GetID() == id 28 | }) 29 | 30 | if !ok { 31 | var result T 32 | return result, fmt.Errorf("not found: %v", id) 33 | } 34 | 35 | return v, nil 36 | } 37 | 38 | func (ms *mockStore[T, O, ID]) FindAll(ctx context.Context, opts O) ([]T, int64, error) { 39 | return ms.store, int64(len(ms.store)), nil 40 | } 41 | 42 | func (ms *mockStore[T, O, ID]) Update(ctx context.Context, id ID, t T) error { 43 | matter, err := ms.Find(ctx, id) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | idx := lo.IndexOf(ms.store, matter) 49 | ms.store[idx] = t 50 | return nil 51 | } 52 | 53 | func (ms *mockStore[T, O, ID]) Delete(ctx context.Context, id ID) error { 54 | matter, err := ms.Find(ctx, id) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | idx := lo.IndexOf(ms.store, matter) 60 | ms.store = append(ms.store[:idx], ms.store[idx+1:]...) 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/mock/recyclebin.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "github.com/saltbo/zpan/internal/app/entity" 5 | "github.com/saltbo/zpan/internal/app/repo" 6 | ) 7 | 8 | var _ repo.RecycleBin = (*RecycleBin)(nil) 9 | 10 | type RecycleBin struct { 11 | mockStore[*entity.RecycleBin, *repo.RecycleBinFindOptions, string] 12 | } 13 | 14 | func NewRecycleBin() *RecycleBin { 15 | return &RecycleBin{} 16 | } 17 | -------------------------------------------------------------------------------- /internal/mock/storage.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "github.com/saltbo/zpan/internal/app/entity" 5 | "github.com/saltbo/zpan/internal/app/repo" 6 | ) 7 | 8 | var _ repo.Storage = (*Storage)(nil) 9 | 10 | type Storage struct { 11 | mockStore[*entity.Storage, *repo.StorageFindOptions, int64] 12 | } 13 | 14 | func NewStorage() *Storage { 15 | return &Storage{} 16 | } 17 | -------------------------------------------------------------------------------- /internal/mock/user.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | "github.com/saltbo/zpan/internal/app/repo" 8 | ) 9 | 10 | var _ repo.User = (*User)(nil) 11 | 12 | type User struct { 13 | } 14 | 15 | func NewUser() *User { 16 | return &User{} 17 | } 18 | 19 | func (u *User) GetUserStorage(ctx context.Context, uid int64) (*entity.UserStorage, error) { 20 | return &entity.UserStorage{Uid: uid, Max: 1000, Used: 10}, nil 21 | } 22 | 23 | func (u *User) UserStorageUsedIncr(ctx context.Context, matter *entity.Matter) error { 24 | return nil 25 | } 26 | 27 | func (u *User) UserStorageUsedDecr(ctx context.Context, matter *entity.Matter) error { 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/pkg/authed/authed.go: -------------------------------------------------------------------------------- 1 | package authed 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/saltbo/zpan/internal/app/model" 7 | ) 8 | 9 | const ( 10 | ctxUidKey = "ctx-uid" 11 | 12 | cookieTokenKey = "z-token" 13 | cookieRoleKey = "z-role" 14 | ) 15 | 16 | func UidSet(c *gin.Context, uid int64) { 17 | c.Set(ctxUidKey, uid) 18 | } 19 | 20 | func UidGet(c *gin.Context) int64 { 21 | return c.GetInt64(ctxUidKey) 22 | } 23 | 24 | func RoleSet(c *gin.Context, roles []string) { 25 | c.Set("role", roles) 26 | } 27 | 28 | func IsAdmin(c *gin.Context) bool { 29 | for _, s := range c.GetStringSlice("role") { 30 | if s == model.RoleAdmin { 31 | return true 32 | } 33 | } 34 | 35 | return false 36 | } 37 | 38 | func TokenCookieSet(c *gin.Context, token string, expireSec int) { 39 | c.SetCookie(cookieTokenKey, token, expireSec, "/", "", false, true) 40 | } 41 | 42 | func TokenCookieGet(c *gin.Context) string { 43 | token, _ := c.Cookie(cookieTokenKey) 44 | return token 45 | } 46 | 47 | func RoleCookieSet(c *gin.Context, token string, expireSec int) { 48 | c.SetCookie(cookieRoleKey, token, expireSec, "/", "", false, false) 49 | } 50 | 51 | func roleCookieGet(c *gin.Context) (string, error) { 52 | return c.Cookie(cookieRoleKey) 53 | } 54 | -------------------------------------------------------------------------------- /internal/pkg/bind/folder.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | import ( 4 | "github.com/saltbo/zpan/internal/app/entity" 5 | ) 6 | 7 | type QueryFolder struct { 8 | QueryPage 9 | 10 | Sid int64 `form:"sid" binding:"required"` 11 | Parent string `form:"parent"` 12 | } 13 | 14 | type BodyFolder struct { 15 | Sid int64 `json:"sid" binding:"required"` 16 | Name string `json:"name" binding:"required"` 17 | Dir string `json:"dir"` 18 | } 19 | 20 | func (p *BodyFolder) ToMatter(uid int64) *entity.Matter { 21 | m := entity.NewMatter(uid, p.Sid, p.Name) 22 | m.Parent = p.Dir 23 | m.DirType = entity.DirTypeUser 24 | return m 25 | } 26 | -------------------------------------------------------------------------------- /internal/pkg/bind/matter.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | import ( 4 | "mime" 5 | "path/filepath" 6 | 7 | "github.com/saltbo/zpan/internal/app/entity" 8 | ) 9 | 10 | type QueryFiles struct { 11 | QueryPage 12 | Sid int64 `form:"sid" binding:"required"` 13 | Dir string `form:"dir"` 14 | Type string `form:"type"` 15 | Keyword string `form:"kw"` 16 | } 17 | 18 | type BodyMatter struct { 19 | Sid int64 `json:"sid" binding:"required"` 20 | Name string `json:"name" binding:"required"` 21 | IsDir bool `json:"is_dir"` 22 | Dir string `json:"dir"` 23 | Type string `json:"type"` 24 | Size int64 `json:"size"` 25 | } 26 | 27 | func (p *BodyMatter) ToMatter(uid int64) *entity.Matter { 28 | detectType := func(name string) string { 29 | cType := mime.TypeByExtension(filepath.Ext(p.Name)) 30 | if cType != "" { 31 | return cType 32 | } 33 | 34 | return "application/octet-stream" 35 | } 36 | 37 | m := entity.NewMatter(uid, p.Sid, p.Name) 38 | m.Type = p.Type 39 | m.Size = p.Size 40 | m.Parent = p.Dir 41 | if p.IsDir { 42 | m.DirType = entity.DirTypeUser 43 | } else if p.Type == "" { 44 | m.Type = detectType(p.Name) 45 | } 46 | return m 47 | } 48 | 49 | type BodyFileRename struct { 50 | NewName string `json:"name" binding:"required"` 51 | } 52 | 53 | type BodyFileMove struct { 54 | NewDir string `json:"dir"` 55 | } 56 | 57 | type BodyFileCopy struct { 58 | NewPath string `json:"path" binding:"required"` 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/bind/query.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | type QueryPage struct { 4 | Offset int `form:"offset"` 5 | Limit int `form:"limit,default=500"` 6 | } 7 | 8 | type QueryPage2 struct { 9 | PageNo int64 `form:"page_no"` 10 | PageSize int64 `form:"page_size,default=20"` 11 | } 12 | -------------------------------------------------------------------------------- /internal/pkg/bind/recycle.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | type QueryRecycle struct { 4 | QueryPage 5 | 6 | Sid int64 `form:"sid" binding:"required"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/pkg/bind/share.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | 4 | type BodyShare struct { 5 | Id int64 `json:"id"` 6 | Matter string `json:"matter"` 7 | Private bool `json:"private"` 8 | ExpireSec int64 `json:"expire_sec"` 9 | } 10 | 11 | type BodyShareDraw struct { 12 | Secret string `form:"secret"` 13 | } 14 | 15 | type QueryShareMatters struct { 16 | QueryPage 17 | Dir string `form:"dir"` 18 | } -------------------------------------------------------------------------------- /internal/pkg/bind/storage.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/saltbo/zpan/internal/app/entity" 7 | ) 8 | 9 | var ts = strings.TrimSpace 10 | 11 | type StorageBody struct { 12 | Mode int8 `json:"mode" binding:"required"` 13 | Name string `json:"name" binding:"required"` 14 | Bucket string `json:"bucket" binding:"required"` 15 | Provider string `json:"provider" binding:"required"` 16 | Endpoint string `json:"endpoint" binding:"required"` 17 | Region string `json:"region"` 18 | AccessKey string `json:"access_key" binding:"required"` 19 | SecretKey string `json:"secret_key" binding:"required"` 20 | PathStyle bool `json:"path_style"` 21 | Title string `json:"title"` 22 | Status int8 `json:"status"` 23 | IDirs string `json:"idirs"` // internal dirs 24 | CustomHost string `json:"custom_host"` 25 | RootPath string `json:"root_path"` 26 | FilePath string `json:"file_path"` 27 | PublicRead bool `json:"public_read"` 28 | } 29 | 30 | func (b *StorageBody) Model() *entity.Storage { 31 | title := b.Title 32 | if title == "" { 33 | title = b.Name 34 | } 35 | 36 | return &entity.Storage{ 37 | Mode: b.Mode, 38 | Name: ts(b.Name), 39 | Title: ts(title), 40 | IDirs: ts(b.IDirs), 41 | Bucket: ts(b.Bucket), 42 | Provider: b.Provider, 43 | PathStyle: b.PathStyle, 44 | Endpoint: ts(b.Endpoint), 45 | Region: ts(b.Region), 46 | Status: b.Status, 47 | CustomHost: ts(b.CustomHost), 48 | AccessKey: ts(b.AccessKey), 49 | SecretKey: ts(b.SecretKey), 50 | RootPath: ts(b.RootPath), 51 | FilePath: ts(b.FilePath), 52 | } 53 | } 54 | 55 | type StorageQuery struct { 56 | QueryPage 57 | 58 | Name string `json:"name"` 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/bind/token.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | type BodyToken struct { 4 | Email string `json:"email" binding:"required"` 5 | Password string `json:"password"` 6 | Captcha string `json:"captcha"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/pkg/bind/user.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | type QueryUser struct { 4 | QueryPage2 5 | Email string `form:"email"` 6 | } 7 | 8 | type BodyUserCreation struct { 9 | Email string `json:"email" binding:"required"` 10 | Password string `json:"password" binding:"required"` 11 | Ticket string `json:"ticket"` 12 | Roles string `json:"roles"` 13 | StorageMax uint64 `json:"storage_max"` 14 | } 15 | 16 | type BodyUserPatch struct { 17 | Token string `json:"token" binding:"required"` 18 | Password string `json:"password"` 19 | Activated bool `json:"activated"` 20 | } 21 | 22 | type BodyUserProfile struct { 23 | Avatar string `json:"avatar"` 24 | Nickname string `json:"nickname"` 25 | Bio string `json:"bio"` 26 | URL string `json:"url"` 27 | Company string `json:"company"` 28 | Location string `json:"location"` 29 | Locale string `json:"locale"` 30 | } 31 | 32 | type BodyUserPassword struct { 33 | OldPassword string `json:"old_password" binding:"required"` 34 | NewPassword string `json:"new_password" binding:"required"` 35 | } 36 | 37 | type BodyUserStorage struct { 38 | Max uint64 `json:"max"` 39 | } 40 | 41 | type BodyUserStatus struct { 42 | Status uint8 `json:"status" binding:"required"` 43 | } 44 | 45 | type BodyUserPasswordReset struct { 46 | Password string `json:"password" binding:"required"` 47 | } 48 | 49 | type BodyUserKeyCreation struct { 50 | Name string `json:"name" binding:"required"` 51 | } 52 | -------------------------------------------------------------------------------- /internal/pkg/gormutil/gorm.go: -------------------------------------------------------------------------------- 1 | package gormutil 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/glebarez/sqlite" 7 | "gorm.io/driver/mysql" 8 | "gorm.io/driver/postgres" 9 | "gorm.io/driver/sqlserver" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | var defaultDB *gorm.DB 14 | 15 | func Init(conf Config, debug bool) { 16 | db, err := New(conf) 17 | if err != nil { 18 | log.Panicln(err) 19 | } 20 | 21 | if debug { 22 | db = db.Debug() 23 | } 24 | 25 | defaultDB = db 26 | } 27 | 28 | func AutoMigrate(models []interface{}) { 29 | defaultDB.AutoMigrate(models...) 30 | } 31 | 32 | func DB() *gorm.DB { 33 | return defaultDB 34 | } 35 | 36 | type Config struct { 37 | Driver string `yaml:"driver"` 38 | DSN string `yaml:"dsn"` 39 | } 40 | 41 | func New(conf Config) (*gorm.DB, error) { 42 | var director func(dsn string) gorm.Dialector 43 | switch conf.Driver { 44 | case "mysql": 45 | director = mysql.Open 46 | case "postgres": 47 | director = postgres.Open 48 | case "sqlserver": 49 | director = sqlserver.Open 50 | default: 51 | director = sqlite.Open 52 | } 53 | 54 | return gorm.Open(director(conf.DSN), &gorm.Config{}) 55 | } 56 | -------------------------------------------------------------------------------- /internal/pkg/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/saltbo/gopkg/ginutil" 11 | "github.com/storyicon/grbac" 12 | "github.com/storyicon/grbac/pkg/meta" 13 | "gopkg.in/yaml.v3" 14 | 15 | "github.com/saltbo/zpan/internal/app/service" 16 | "github.com/saltbo/zpan/internal/pkg/authed" 17 | ) 18 | 19 | //go:embed auth_rbac.yml 20 | var embedRules []byte 21 | 22 | func LoginAuth() gin.HandlerFunc { 23 | return LoginAuthWithRoles() 24 | } 25 | 26 | func LoginAuthWithRoles() gin.HandlerFunc { 27 | rules := make(meta.Rules, 0) 28 | if err := yaml.Unmarshal(embedRules, &rules); err != nil { 29 | log.Fatalln(err) 30 | } 31 | 32 | ctrl, err := grbac.New(grbac.WithRules(rules)) 33 | if err != nil { 34 | log.Fatalln(err) 35 | } 36 | 37 | return func(c *gin.Context) { 38 | rc, err := token2Roles(c) 39 | if err != nil { 40 | ginutil.JSONUnauthorized(c, err) 41 | return 42 | } 43 | 44 | state, err := ctrl.IsRequestGranted(c.Request, rc.Roles) 45 | if err != nil { 46 | ginutil.JSONServerError(c, err) 47 | return 48 | } 49 | 50 | if rc.Subject == "anonymous" && !state.IsGranted() { 51 | ginutil.JSONUnauthorized(c, fmt.Errorf("access deny, should login")) 52 | return 53 | } 54 | 55 | if !state.IsGranted() { 56 | ginutil.JSONForbidden(c, fmt.Errorf("access deny")) 57 | return 58 | } 59 | 60 | authed.UidSet(c, rc.Uid()) 61 | authed.RoleSet(c, rc.Roles) 62 | } 63 | } 64 | 65 | func token2Roles(c *gin.Context) (*service.RoleClaims, error) { 66 | const basicPrefix = "Basic " 67 | const BearerPrefix = "Bearer " 68 | cookieAuth := authed.TokenCookieGet(c) 69 | headerAuth := c.GetHeader("Authorization") 70 | if (cookieAuth == "" && headerAuth == "") || strings.HasPrefix(headerAuth, basicPrefix) { 71 | return service.NewRoleClaims("anonymous", 3600, []string{"guest"}), nil 72 | } 73 | 74 | authToken := strings.TrimPrefix(headerAuth, BearerPrefix) 75 | if authToken == "" { 76 | authToken = cookieAuth 77 | } 78 | 79 | return service.NewToken().Verify(authToken) 80 | } 81 | -------------------------------------------------------------------------------- /internal/pkg/middleware/auth_rbac.yml: -------------------------------------------------------------------------------- 1 | # 默认注册用户才能访问 2 | - id: 0 3 | host: "*" 4 | path: "**" 5 | method: "*" 6 | authorized_roles: 7 | - "admin" 8 | - "member" 9 | 10 | # 站点信息允许匿名访问 11 | - id: 10 12 | host: "*" 13 | path: "/api/system/options/core.site" 14 | method: "GET" 15 | allow_anyone: true 16 | 17 | # 登录接口允许任何人访问 18 | - id: 11 19 | host: "*" 20 | path: "/api/tokens" 21 | method: "{POST,DELETE}" 22 | allow_anyone: true 23 | # 注册接口允许任何人访问 24 | - id: 12 25 | host: "*" 26 | path: "/api/users" 27 | method: "{POST,PATCH}" 28 | allow_anyone: true 29 | # 分享接口允许匿名访问 30 | - id: 13 31 | host: "*" 32 | path: "/api/shares/**" 33 | method: "GET" 34 | allow_anyone: true 35 | 36 | # 分享提取接口允许匿名访问 37 | - id: 14 38 | host: "*" 39 | path: "/api/shares/*/token" 40 | method: "POST" 41 | allow_anyone: true 42 | 43 | # 下载接口允许匿名访问 44 | - id: 15 45 | host: "*" 46 | path: "/api/matters/*/link" 47 | method: "GET" 48 | allow_anyone: true 49 | 50 | 51 | # 以下规则限制只能由管理员请求 52 | - id: 101 53 | host: "*" 54 | path: "/api/storages" 55 | method: "POST" 56 | authorized_roles: 57 | - "admin" 58 | 59 | - id: 102 60 | host: "*" 61 | path: "/api/storages/**" 62 | method: "{PUT,PATCH,DELETE}" 63 | authorized_roles: 64 | - "admin" 65 | 66 | - id: 103 67 | host: "*" 68 | path: "/api/users" 69 | method: "GET" 70 | authorized_roles: 71 | - "admin" 72 | 73 | - id: 104 74 | host: "*" 75 | path: "/api/users/**" 76 | method: "{PUT,DELETE}" 77 | authorized_roles: 78 | - "admin" 79 | 80 | - id: 105 81 | host: "*" 82 | path: "/api/system/options/*" 83 | method: "PUT" 84 | authorized_roles: 85 | - "admin" 86 | 87 | - id: 106 88 | host: "*" 89 | path: "/api/system/options/core.email" 90 | method: "GET" 91 | authorized_roles: 92 | - "admin" 93 | -------------------------------------------------------------------------------- /internal/pkg/middleware/installer.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/saltbo/gopkg/ginutil" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | func Installer(c *gin.Context) { 12 | if !viper.IsSet("installed") { 13 | ginutil.JSONError(c, 520, fmt.Errorf("system is not initialized")) 14 | return 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/pkg/middleware/rbac_roles.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | //var defaultRules = grbac.Rules{ 4 | // { 5 | // Resource: &meta.Resource{ 6 | // Host: "*", 7 | // Path: "**", 8 | // Method: "*", 9 | // }, 10 | // Permission: &meta.Permission{ 11 | // AuthorizedRoles: []string{"admin", "member"}, 12 | // }, 13 | // }, 14 | // { 15 | // Resource: &meta.Resource{ 16 | // Host: "*", 17 | // Path: "/api/v1/tokens", 18 | // Method: "POST", 19 | // }, 20 | // Permission: &meta.Permission{ 21 | // AllowAnyone: true, 22 | // }, 23 | // }, 24 | // { 25 | // Resource: &meta.Resource{ 26 | // Host: "*", 27 | // Path: "/api/v1/users", 28 | // Method: "GET", 29 | // }, 30 | // Permission: &meta.Permission{ 31 | // AuthorizedRoles: []string{"admin"}, 32 | // }, 33 | // }, 34 | //} 35 | -------------------------------------------------------------------------------- /internal/pkg/middleware/roles.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | //func Roles() grbac.Rules { 4 | // return grbac.Rules{ 5 | // { 6 | // Resource: &meta.Resource{ 7 | // Host: "*", 8 | // Path: "/api/files", 9 | // Method: "{GET,POST}", 10 | // }, 11 | // Permission: &meta.Permission{ 12 | // AllowAnyone: true, 13 | // }, 14 | // }, 15 | // { 16 | // Resource: &meta.Resource{ 17 | // Host: "*", 18 | // Path: "/s/**", 19 | // Method: "{GET}", 20 | // }, 21 | // Permission: &meta.Permission{ 22 | // AllowAnyone: true, 23 | // }, 24 | // }, 25 | // { 26 | // Resource: &meta.Resource{ 27 | // Host: "*", 28 | // Path: "/api/shares/**", 29 | // Method: "{GET}", 30 | // }, 31 | // Permission: &meta.Permission{ 32 | // AllowAnyone: true, 33 | // }, 34 | // }, 35 | // } 36 | //} 37 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | var urlEncode = url.QueryEscape 12 | 13 | var ( 14 | corsAllowMethods = []string{"GET", "PUT"} 15 | corsAllowHeaders = []string{"content-type", "content-disposition", "x-amz-acl"} 16 | ) 17 | 18 | const ( 19 | defaultUploadExp = time.Hour 20 | defaultDownloadExp = time.Hour * 24 21 | ) 22 | 23 | // Object is the basic operation unit 24 | type Object struct { 25 | Key string // remote file path 26 | ETag string // file md5 27 | FilePath string // local file path 28 | Type string // local file type, added or changed 29 | } 30 | 31 | type Provider interface { 32 | SetupCORS() error 33 | Head(object string) (*Object, error) 34 | List(prefix string) ([]Object, error) 35 | Move(object, newObject string) error 36 | SignedPutURL(key, filetype string, filesize int64, public bool) (url string, headers http.Header, err error) 37 | SignedGetURL(key, filename string) (url string, err error) 38 | PublicURL(key string) (url string) 39 | ObjectDelete(key string) error 40 | ObjectsDelete(keys []string) error 41 | } 42 | 43 | type Config struct { 44 | Provider string 45 | Bucket string 46 | Endpoint string 47 | Region string 48 | CustomHost string 49 | AccessKey string 50 | AccessSecret string 51 | PathStyle bool 52 | } 53 | 54 | func (c *Config) Clone() *Config { 55 | clone := *c 56 | return &clone 57 | } 58 | 59 | func (c *Config) WithCustomHost(s string) *Config { 60 | c.CustomHost = s 61 | return c 62 | } 63 | 64 | type Constructor func(provider *Config) (Provider, error) 65 | 66 | var supportProviders = map[string]Constructor{ 67 | "COS": NewCOSProvider, 68 | "KODO": NewKODOProvider, 69 | "MINIO": NewMINIOProvider, 70 | "NOS": NewNOSProvider, 71 | "OBS": NewOBSProvider, 72 | "OSS": NewOSSProvider, 73 | "S3": NewS3Provider, 74 | "US3": NewUS3Provider, 75 | "USS": NewUSSProvider, 76 | // "od": NewODProvider, 77 | // "gd": NewGDProvider, 78 | } 79 | 80 | func New(conf *Config) (Provider, error) { 81 | if conf.Region == "" { 82 | conf.Region = "auto" 83 | } 84 | 85 | if conf.CustomHost != "" && !strings.Contains(conf.CustomHost, "://") { 86 | conf.CustomHost = "http://" + conf.CustomHost 87 | } 88 | 89 | constructor, ok := supportProviders[strings.ToUpper(conf.Provider)] 90 | if !ok { 91 | return nil, fmt.Errorf("provider %s not found", conf.Provider) 92 | } 93 | 94 | return constructor(conf) 95 | } 96 | 97 | func GetProviders() []string { 98 | keys := make([]string, 0) 99 | for k := range supportProviders { 100 | keys = append(keys, k) 101 | } 102 | 103 | return keys 104 | } 105 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_cos.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | 10 | "github.com/tencentyun/cos-go-sdk-v5" 11 | ) 12 | 13 | // COSProvider 腾讯云 14 | type COSProvider struct { 15 | S3Provider 16 | 17 | client *cos.Client 18 | } 19 | 20 | func NewCOSProvider(conf *Config) (Provider, error) { 21 | p, err := newS3Provider(conf) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | u, err := url.Parse(fmt.Sprintf("https://%s.%s", conf.Bucket, conf.Endpoint)) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | httpClient := &http.Client{ 32 | Timeout: 100 * time.Second, 33 | Transport: &cos.AuthorizationTransport{ 34 | SecretID: conf.AccessKey, 35 | SecretKey: conf.AccessSecret, 36 | }, 37 | } 38 | 39 | return &COSProvider{ 40 | S3Provider: *p, 41 | 42 | client: cos.NewClient(&cos.BaseURL{BucketURL: u}, httpClient), 43 | }, err 44 | } 45 | 46 | func (p *COSProvider) SetupCORS() error { 47 | var existRules []cos.BucketCORSRule 48 | ctx := context.Background() 49 | ret, _, _ := p.client.Bucket.GetCORS(ctx) 50 | if ret != nil && len(ret.Rules) > 0 { 51 | existRules = append(existRules, ret.Rules...) 52 | } 53 | 54 | zRule := cos.BucketCORSRule{ 55 | AllowedOrigins: []string{"*"}, 56 | AllowedMethods: corsAllowMethods, 57 | AllowedHeaders: corsAllowHeaders, 58 | MaxAgeSeconds: 300, 59 | } 60 | _, err := p.client.Bucket.PutCORS(ctx, &cos.BucketPutCORSOptions{Rules: append(existRules, zRule)}) 61 | return err 62 | } 63 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_kodo.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 5 | ) 6 | 7 | // KODOProvider 七牛云 8 | type KODOProvider struct { 9 | S3Provider 10 | 11 | client *oss.Client 12 | } 13 | 14 | func NewKODOProvider(conf *Config) (Provider, error) { 15 | p, err := newS3Provider(conf) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &KODOProvider{ 21 | S3Provider: *p, 22 | }, err 23 | } 24 | 25 | func (p *KODOProvider) SetupCORS() error { 26 | // 官方没有提供相关接口,但是兼容S3的接口 27 | return p.S3Provider.SetupCORS() 28 | } 29 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_minio.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 5 | ) 6 | 7 | // MINIOProvider MinIO 8 | type MINIOProvider struct { 9 | S3Provider 10 | 11 | client *oss.Client 12 | } 13 | 14 | func NewMINIOProvider(conf *Config) (Provider, error) { 15 | p, err := newS3Provider(conf) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &MINIOProvider{ 21 | S3Provider: *p, 22 | }, err 23 | } 24 | 25 | func (p *MINIOProvider) SetupCORS() error { 26 | // 没找到相关接口,好像是没有跨域限制? 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_mock.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type MockProvider struct { 9 | } 10 | 11 | func NewMockProvider(provider *Config) (Provider, error) { 12 | return &MockProvider{}, nil 13 | } 14 | 15 | func (m *MockProvider) SetupCORS() error { 16 | return nil 17 | } 18 | 19 | func (m *MockProvider) Head(object string) (*Object, error) { 20 | return &Object{ 21 | Key: "20210709/86JzAOLIlGZ5Z2Fk.zip", 22 | Type: "application/zip", 23 | }, nil 24 | } 25 | 26 | func (m *MockProvider) List(prefix string) ([]Object, error) { 27 | return []Object{}, nil 28 | } 29 | 30 | func (m *MockProvider) Move(object, newObject string) error { 31 | return nil 32 | } 33 | 34 | func (m *MockProvider) SignedPutURL(key, filetype string, filesize int64, public bool) (url string, headers http.Header, err error) { 35 | headers = make(http.Header) 36 | headers.Add("", "") 37 | return fmt.Sprintf("http://dl.test.com/%s", key), headers, nil 38 | } 39 | 40 | func (m *MockProvider) SignedGetURL(key, filename string) (url string, err error) { 41 | return fmt.Sprintf("http://dl.test.com/%s", key), nil 42 | } 43 | 44 | func (m *MockProvider) PublicURL(key string) (url string) { 45 | return fmt.Sprintf("http://dl.test.com/%s", key) 46 | } 47 | 48 | func (m *MockProvider) ObjectDelete(key string) error { 49 | return nil 50 | } 51 | 52 | func (m *MockProvider) ObjectsDelete(keys []string) error { 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_nos.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/saltbo/zpan/pkg/nos/config" 5 | "github.com/saltbo/zpan/pkg/nos/nosclient" 6 | ) 7 | 8 | // NOSProvider 网易云 9 | type NOSProvider struct { 10 | S3Provider 11 | 12 | client *nosclient.NosClient 13 | } 14 | 15 | func NewNOSProvider(conf *Config) (Provider, error) { 16 | cfg := &config.Config{ 17 | Endpoint: conf.Endpoint, 18 | AccessKey: conf.AccessKey, 19 | SecretKey: conf.AccessSecret, 20 | NosServiceConnectTimeout: 3, 21 | NosServiceReadWriteTimeout: 10, 22 | NosServiceMaxIdleConnection: 100, 23 | } 24 | client, err := nosclient.New(cfg) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | p, err := newS3Provider(conf) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return &NOSProvider{ 35 | S3Provider: *p, 36 | 37 | client: client, 38 | }, err 39 | } 40 | 41 | func (p *NOSProvider) SetupCORS() error { 42 | // p.client. 43 | // 官方的sdk里没有相关方法,暂不实现 44 | // todo 查询官方的API文档发现是有CORS相关接口的,可以给官方提交个PR 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_obs.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/saltbo/zpan/pkg/obs" 5 | ) 6 | 7 | // OBSProvider 华为云 8 | type OBSProvider struct { 9 | S3Provider 10 | 11 | client *obs.ObsClient 12 | } 13 | 14 | func NewOBSProvider(conf *Config) (Provider, error) { 15 | client, err := obs.New(conf.AccessKey, conf.AccessSecret, conf.Endpoint) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | p, err := newS3Provider(conf) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return &OBSProvider{ 26 | S3Provider: *p, 27 | 28 | client: client, 29 | }, err 30 | } 31 | 32 | func (p *OBSProvider) SetupCORS() error { 33 | var existRules []obs.CorsRule 34 | ret, _ := p.client.GetBucketCors(p.bucket) 35 | if ret != nil && len(ret.CorsRules) > 0 { 36 | existRules = append(existRules, ret.CorsRules...) 37 | } 38 | 39 | zRule := obs.CorsRule{ 40 | AllowedOrigin: []string{"*"}, 41 | AllowedMethod: corsAllowMethods, 42 | AllowedHeader: corsAllowHeaders, 43 | MaxAgeSeconds: 300, 44 | } 45 | input := &obs.SetBucketCorsInput{ 46 | Bucket: p.bucket, 47 | BucketCors: obs.BucketCors{CorsRules: append(existRules, zRule)}, 48 | } 49 | _, err := p.client.SetBucketCors(input) 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_oss.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 5 | ) 6 | 7 | // OSSProvider 阿里云 8 | type OSSProvider struct { 9 | S3Provider 10 | 11 | client *oss.Client 12 | } 13 | 14 | func NewOSSProvider(conf *Config) (Provider, error) { 15 | client, err := oss.New(conf.Endpoint, conf.AccessKey, conf.AccessSecret) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | p, err := newS3Provider(conf) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return &OSSProvider{ 26 | S3Provider: *p, 27 | 28 | client: client, 29 | }, err 30 | } 31 | 32 | func (p *OSSProvider) SetupCORS() error { 33 | var existRules []oss.CORSRule 34 | ret, _ := p.client.GetBucketCORS(p.bucket) 35 | if len(ret.CORSRules) > 0 { 36 | existRules = append(existRules, ret.CORSRules...) 37 | } 38 | 39 | zRule := oss.CORSRule{ 40 | AllowedOrigin: []string{"*"}, 41 | AllowedMethod: corsAllowMethods, 42 | AllowedHeader: corsAllowHeaders, 43 | MaxAgeSeconds: 300, 44 | } 45 | return p.client.SetBucketCORS(p.bucket, append(existRules, zRule)) 46 | } 47 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/aws/aws-sdk-go/service/s3" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // default config 15 | var dc = &Config{ 16 | Provider: "s3", 17 | Bucket: "test-bucket", 18 | Endpoint: "s3.ap-northeast-1.amazonaws.com", 19 | AccessKey: "test-ak", 20 | AccessSecret: "test-sk", 21 | } 22 | 23 | var key = "1001/test.txt" 24 | 25 | func assertSignedURL(t *testing.T, err error, us, customHost string) *url.URL { 26 | assert.NoError(t, err) 27 | host := fmt.Sprintf("%s.%s", dc.Bucket, dc.Endpoint) 28 | if customHost != "" { 29 | host = strings.TrimPrefix(customHost, "http://") 30 | } 31 | u, err := url.Parse(us) 32 | assert.NoError(t, err) 33 | assert.Equal(t, "/"+key, u.Path) 34 | assert.Equal(t, host, u.Host) 35 | assert.Contains(t, u.RawQuery, dc.AccessKey) 36 | assert.NotContains(t, u.RawQuery, dc.AccessSecret) 37 | return u 38 | } 39 | 40 | func TestSignedPutURL(t *testing.T) { 41 | disk, err := New(dc) 42 | assert.NoError(t, err) 43 | 44 | _, headers, err := disk.SignedPutURL(key, "text/plain", 0, false) 45 | assert.NoError(t, err) 46 | 47 | assert.Equal(t, s3.ObjectCannedACLPrivate, headers.Get("x-amz-acl")) 48 | assert.Equal(t, "text/plain", headers.Get("content-type")) 49 | } 50 | 51 | func testSignedGetURL(t *testing.T, cfg *Config) { 52 | disk, err := New(cfg) 53 | assert.NoError(t, err) 54 | 55 | filename := "test2.txt" 56 | us, err := disk.SignedGetURL(key, filename) 57 | u := assertSignedURL(t, err, us, cfg.CustomHost) 58 | assert.Equal(t, u.Query().Get("response-content-disposition"), fmt.Sprintf(`attachment;filename="%s"`, urlEncode(filename))) 59 | } 60 | 61 | func TestSignedGetURL(t *testing.T) { 62 | configs := []*Config{ 63 | dc.Clone().WithCustomHost(""), 64 | dc.Clone().WithCustomHost("dl.zpan.com"), 65 | dc.Clone().WithCustomHost("http://dl.zpan.com"), 66 | } 67 | 68 | for idx, config := range configs { 69 | t.Run(strconv.Itoa(idx), func(t *testing.T) { 70 | testSignedGetURL(t, config) 71 | }) 72 | } 73 | } 74 | 75 | func TestPublicURL(t *testing.T) { 76 | disk, err := New(dc) 77 | assert.NoError(t, err) 78 | 79 | us := disk.PublicURL(key) 80 | u, err := url.Parse(us) 81 | assert.NoError(t, err) 82 | assert.Equal(t, "/"+key, u.Path) 83 | assert.Equal(t, fmt.Sprintf("%s.%s", dc.Bucket, dc.Endpoint), u.Host) 84 | } 85 | 86 | func TestNotSupportedProvider(t *testing.T) { 87 | conf := dc 88 | conf.Provider = "test-provider" 89 | _, err := New(conf) 90 | assert.Error(t, err) 91 | } 92 | 93 | func TestNew4Storage(t *testing.T) { 94 | conf := dc 95 | conf.Provider = "s3" 96 | _, err := New(conf) 97 | assert.NoError(t, err) 98 | } 99 | -------------------------------------------------------------------------------- /internal/pkg/provider/provider_us3.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 5 | ) 6 | 7 | // US3Provider UCloud 8 | type US3Provider struct { 9 | S3Provider 10 | 11 | client *oss.Client 12 | } 13 | 14 | func NewUS3Provider(conf *Config) (Provider, error) { 15 | p, err := newS3Provider(conf) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &US3Provider{ 21 | S3Provider: *p, 22 | }, err 23 | } 24 | 25 | func (p *US3Provider) SetupCORS() error { 26 | // 官方没有提供相关接口,暂不实现 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Ambor 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package main 23 | 24 | import ( 25 | "github.com/saltbo/zpan/cmd" 26 | ) 27 | 28 | func main() { 29 | cmd.Execute() 30 | } 31 | -------------------------------------------------------------------------------- /pkg/nos/auth/nosauth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "net/http" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | var subResources map[string]bool = map[string]bool{ 13 | "acl": true, 14 | "location": true, 15 | "versioning": true, 16 | "versions": true, 17 | "versionId": true, 18 | "uploadId": true, 19 | "uploads": true, 20 | "partNumber": true, 21 | "delete": true, 22 | "deduplication": true, 23 | } 24 | 25 | func SignRequest(request *http.Request, publicKey string, secretKey string, 26 | bucket string, encodedObject string) string { 27 | 28 | stringToSign := "" 29 | stringToSign += (request.Method + "\n") 30 | stringToSign += (request.Header.Get("Content-MD5") + "\n") 31 | stringToSign += (request.Header.Get("Content-Type") + "\n") 32 | stringToSign += (request.Header.Get("Date") + "\n") 33 | 34 | var headerKeys sort.StringSlice 35 | for origKey, _ := range request.Header { 36 | key := strings.ToLower(origKey) 37 | if strings.HasPrefix(key, "x-nos-") { 38 | headerKeys = append(headerKeys, origKey) 39 | } 40 | } 41 | 42 | headerKeys.Sort() 43 | 44 | for i := 0; i < headerKeys.Len(); i++ { 45 | key := strings.ToLower(headerKeys[i]) 46 | stringToSign += (key + ":" + request.Header.Get(headerKeys[i]) + "\n") 47 | } 48 | 49 | stringToSign += (getResource(bucket, encodedObject)) 50 | 51 | request.ParseForm() 52 | 53 | var keys sort.StringSlice 54 | for key := range request.Form { 55 | if _, ok := subResources[key]; ok { 56 | keys = append(keys, key) 57 | } 58 | } 59 | keys.Sort() 60 | 61 | for i := 0; i < keys.Len(); i++ { 62 | if i == 0 { 63 | stringToSign += "?" 64 | } 65 | stringToSign += keys[i] 66 | if val := request.Form[keys[i]]; val[0] != "" { 67 | stringToSign += ("=" + val[0]) 68 | } 69 | 70 | if i < keys.Len()-1 { 71 | stringToSign += "&" 72 | } 73 | } 74 | key := []byte(secretKey) 75 | h := hmac.New(sha256.New, key) 76 | h.Write([]byte(stringToSign)) 77 | return "NOS " + publicKey + ":" + base64.StdEncoding.EncodeToString(h.Sum(nil)) 78 | } 79 | 80 | func getResource(bucket string, encodedObject string) string { 81 | resource := "/" 82 | if bucket != "" { 83 | resource += bucket + "/" 84 | } 85 | if encodedObject != "" { 86 | resource += encodedObject 87 | } 88 | return resource 89 | } 90 | -------------------------------------------------------------------------------- /pkg/nos/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/saltbo/zpan/pkg/nos/logger" 5 | "github.com/saltbo/zpan/pkg/nos/noserror" 6 | "github.com/saltbo/zpan/pkg/nos/utils" 7 | ) 8 | 9 | type Config struct { 10 | Endpoint string 11 | AccessKey string 12 | SecretKey string 13 | 14 | NosServiceConnectTimeout int 15 | NosServiceReadWriteTimeout int 16 | NosServiceMaxIdleConnection int 17 | 18 | LogLevel *logger.LogLevelType 19 | 20 | Logger logger.Logger 21 | 22 | IsSubDomain *bool 23 | } 24 | 25 | func (conf *Config) SetIsSubDomain(isSubDomain bool) error { 26 | conf.IsSubDomain = &isSubDomain 27 | return nil 28 | } 29 | 30 | func (conf *Config) GetIsSubDomain() bool { 31 | if conf.IsSubDomain == nil { 32 | return true 33 | } else { 34 | return *conf.IsSubDomain 35 | } 36 | } 37 | 38 | func (conf *Config) Check() error { 39 | if conf.Endpoint == "" { 40 | return utils.ProcessClientError(noserror.ERROR_CODE_CFG_ENDPOINT, "", "", "") 41 | } 42 | 43 | if conf.NosServiceConnectTimeout < 0 { 44 | return utils.ProcessClientError(noserror.ERROR_CODE_CFG_CONNECT_TIMEOUT, "", "", "") 45 | } 46 | 47 | if conf.NosServiceReadWriteTimeout < 0 { 48 | return utils.ProcessClientError(noserror.ERROR_CODE_CFG_READWRITE_TIMEOUT, "", "", "") 49 | } 50 | 51 | if conf.NosServiceMaxIdleConnection < 0 { 52 | return utils.ProcessClientError(noserror.ERROR_CODE_CFG_MAXIDLECONNECT, "", "", "") 53 | } 54 | 55 | if conf.NosServiceConnectTimeout == 0 { 56 | conf.NosServiceConnectTimeout = 30 57 | } 58 | 59 | if conf.NosServiceReadWriteTimeout == 0 { 60 | conf.NosServiceReadWriteTimeout = 60 61 | } 62 | 63 | if conf.NosServiceMaxIdleConnection == 0 { 64 | conf.NosServiceMaxIdleConnection = 60 65 | } 66 | 67 | if conf.Logger == nil { 68 | conf.Logger = logger.NewDefaultLogger() 69 | } 70 | 71 | if conf.LogLevel == nil { 72 | conf.LogLevel = logger.LogLevel(logger.DEBUG) 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/nos/nosconst/nosconst.go: -------------------------------------------------------------------------------- 1 | package nosconst 2 | 3 | const ( 4 | HZ = iota 5 | ) 6 | 7 | type Location int 8 | type Acl int 9 | 10 | const ( 11 | PRIVATE = iota 12 | PUBLICREAD 13 | ) 14 | 15 | const ( 16 | DEFAULT_MAXBUFFERSIZE = 1024 * 1024 17 | MAX_FILESIZE = 100 * 1024 * 1024 18 | MIN_FILESIZE = 16 * 1024 19 | MAX_FILENUMBER = 1000 20 | DEFAULTVALUE = 1000 21 | MAX_DELETEBODY = 2 * 1024 * 1024 22 | 23 | RFC1123_NOS = "Mon, 02 Jan 2006 15:04:05 Asia/Shanghai" 24 | RFC1123_GMT = "Mon, 02 Jan 2006 15:04:05 GMT" 25 | CONTENT_LENGTH = "Content-Length" 26 | CONTENT_TYPE = "Content-Type" 27 | CONTENT_MD5 = "Content-Md5" 28 | LAST_MODIFIED = "Last-Modified" 29 | USER_AGENT = "User-Agent" 30 | DATE = "Date" 31 | AUTHORIZATION = "Authorization" 32 | RANGE = "Range" 33 | IfMODIFYSINCE = "If-Modified-Since" 34 | LIST_PREFIX = "prefix" 35 | LIST_DELIMITER = "delimiter" 36 | LIST_MARKER = "marker" 37 | LIST_MAXKEYS = "max-keys" 38 | UPLOADID = "uploadId" 39 | MAX_PARTS = "max-parts" 40 | PARTNUMBER = "partNumber" 41 | UPLOADS = "uploads" 42 | PART_NUMBER_MARKER = "part-number-marker" 43 | LIST_KEY_MARKER = "key-marker" 44 | LIST_MAX_UPLOADS = "max-uploads" 45 | LIST_UPLOADID_MARKER = "upload-id-marker" 46 | 47 | ETAG = "Etag" 48 | NOS_USER_METADATA_PREFIX = "X-Nos-Meta-" 49 | NOS_ENTITY_TYPE = "X-Nos-Entity-Type" 50 | NOS_VERSION_ID = "X-Nos-Version-Id" 51 | X_NOS_OBJECT_NAME = "X-Nos-Object-Name" 52 | X_NOS_REQUEST_ID = "X-Nos-Request-Id" 53 | X_NOS_OBJECT_MD5 = "X-Nos-Object-Md5" 54 | X_NOS_COPY_SOURCE = "x-nos-copy-source" 55 | X_NOS_MOVE_SOURCE = "x-nos-move-source" 56 | X_NOS_ACL = "x-nos-acl" 57 | 58 | ORIG_CONTENT_MD5 = "Content-MD5" 59 | ORIG_ETAG = "ETag" 60 | ORIG_NOS_USER_METADATA_PREFIX = "x-nos-meta-" 61 | ORIG_NOS_VERSION_ID = "x-nos-version-id" 62 | ORIG_X_NOS_OBJECT_NAME = "x-nos-object-name" 63 | ORIG_X_NOS_REQUEST_ID = "x-nos-request-id" 64 | ORIG_X_NOS_OBJECT_MD5 = "x-nos-Object-md5" 65 | 66 | SDKNAME = "nos-golang-sdk" 67 | VERSION = "1.0.0" 68 | 69 | JSON_TYPE = "json" 70 | XML_TYPE = "xml" 71 | ) 72 | -------------------------------------------------------------------------------- /pkg/nos/sample.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/saltbo/zpan/pkg/nos/config" 7 | "github.com/saltbo/zpan/pkg/nos/logger" 8 | "github.com/saltbo/zpan/pkg/nos/model" 9 | "github.com/saltbo/zpan/pkg/nos/nosclient" 10 | ) 11 | 12 | func sample_init(endpoint, accessKey, secretKey string) *nosclient.NosClient { 13 | conf := &config.Config{ 14 | Endpoint: endpoint, 15 | AccessKey: accessKey, 16 | SecretKey: secretKey, 17 | 18 | NosServiceConnectTimeout: 3, 19 | NosServiceReadWriteTimeout: 60, 20 | NosServiceMaxIdleConnection: 100, 21 | 22 | LogLevel: logger.LogLevel(logger.DEBUG), 23 | Logger: logger.NewDefaultLogger(), 24 | } 25 | 26 | nosClient, _ := nosclient.New(conf) 27 | return nosClient 28 | } 29 | 30 | func main() { 31 | path := "" 32 | endpoint := "" 33 | accessKey := "" 34 | secretKey := "" 35 | 36 | nosClient := sample_init(endpoint, accessKey, secretKey) 37 | 38 | putObjectRequest := &model.PutObjectRequest{ 39 | Bucket: "", 40 | Object: "", 41 | FilePath: path, 42 | } 43 | _, err := nosClient.PutObjectByFile(putObjectRequest) 44 | if err != nil { 45 | fmt.Println(err.Error()) 46 | } 47 | 48 | getObjectRequest := &model.GetObjectRequest{ 49 | Bucket: "", 50 | Object: "", 51 | } 52 | objectResult, err := nosClient.GetObject(getObjectRequest) 53 | if err != nil { 54 | fmt.Println(err.Error()) 55 | } else { 56 | objectResult.Body.Close() 57 | } 58 | 59 | objectRequest := &model.ObjectRequest{ 60 | Bucket: "", 61 | Object: "", 62 | } 63 | err = nosClient.DeleteObject(objectRequest) 64 | if err != nil { 65 | fmt.Println(err.Error()) 66 | } 67 | 68 | fmt.Println("Simple samples completed") 69 | } 70 | -------------------------------------------------------------------------------- /pkg/nos/test/fortest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/pkg/nos/test/fortest -------------------------------------------------------------------------------- /pkg/nos/test/testserver: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/pkg/nos/test/testserver -------------------------------------------------------------------------------- /pkg/nos/test/testserver2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/pkg/nos/test/testserver2 -------------------------------------------------------------------------------- /pkg/nos/tools/cover.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # used in converage html generation for golang project 3 | # put this shell in project's dir 4 | curPath=`pwd` 5 | # export GOPATH=$curPath/../../.. 6 | ROOT_DIR=`pwd`/.. 7 | COVERAGE_FILE=`pwd`/coverage.html # filepath to put converage 8 | 9 | ##package need to ignore 10 | declare -a ignorePackage=("github.com" "gopkg.in" "logger" "tools" ,"test") 11 | 12 | subdirs=`ls $ROOT_DIR` 13 | prefix='{"Packages":[' 14 | suffix=']}' 15 | empty_result='{"Packages":null}' 16 | left=${#prefix} ##left postion of json result for a package 17 | 18 | # 19 | # check and cd package if package legall 20 | # 21 | check_and_cd_package() 22 | { 23 | pkg=$1 24 | curDir=$ROOT_DIR/$pkg 25 | #echo $pkg 26 | #echo $ROOT_DIR 27 | echo "curDir:"$curDir 28 | for ipkg in ${ignorePackage[@]}; 29 | do 30 | # echo "echo $pkg | grep $ipkg" 31 | info=`echo $pkg| grep $ipkg` 32 | if [ "$info" != "" ]; then 33 | # echo "ignorePackage $pkg" 34 | return 1 35 | fi 36 | done 37 | isDir=`test -d $curDir` 38 | if [ "$?" != "0" ];then 39 | return 1 40 | fi 41 | cd $curDir 42 | return $? 43 | } 44 | 45 | # 46 | # get current package's converge 47 | # 48 | get_package_coverage(){ 49 | 50 | result=`gocov test` 51 | 52 | if [ "$?" != "0" ] || [ "$result" == "" ] ; then 53 | return 1 54 | fi 55 | if [ "$result" == "$empty_result" ]; then 56 | return 1 57 | fi 58 | 59 | right=$((${#result}-${#suffix}-$left)) 60 | cur_result=${result:$left:$right} 61 | echo $cur_result 62 | return 0 63 | } 64 | 65 | 66 | json_result="" 67 | for pkg in $subdirs; 68 | do 69 | check_and_cd_package $pkg 70 | 71 | if [ "$?" != "0" ] ; then 72 | continue 73 | fi 74 | #cd $curDir 75 | cur_result=$(get_package_coverage) 76 | if [ "$?" != "0" ] ; then 77 | continue 78 | fi 79 | 80 | echo "Get package $pkg's coverage success" 81 | 82 | if [ "$json_result" != "" ]; then 83 | json_result="$json_result," 84 | fi 85 | 86 | json_result=$json_result$cur_result 87 | #echo "*************current result***************" 88 | #echo $cur_result 89 | #echo "***************current result end*********" 90 | #echo "PSW:"$ROOT_DIR 91 | done 92 | 93 | #echo "-----------------------" 94 | 95 | total_result=$prefix$json_result$suffix 96 | echo $total_result | gocov-html > $COVERAGE_FILE 97 | #echo $total_result 98 | -------------------------------------------------------------------------------- /pkg/nos/tools/run_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cur=`pwd` 4 | # export GOPATH=$cur/../../../ 5 | 6 | curPath=`pwd`/.. 7 | for dir in $curPath/* 8 | do 9 | echo $dir 10 | test -d $dir && cd $dir && go test 11 | done 12 | -------------------------------------------------------------------------------- /pkg/obs/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Huawei Technologies Co.,Ltd. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use 3 | // this file except in compliance with the License. You may obtain a copy of the 4 | // License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software distributed 9 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations under the License. 12 | 13 | package obs 14 | 15 | import ( 16 | "encoding/xml" 17 | "fmt" 18 | ) 19 | 20 | // ObsError defines error response from OBS 21 | type ObsError struct { 22 | BaseModel 23 | Status string 24 | XMLName xml.Name `xml:"Error"` 25 | Code string `xml:"Code" json:"code"` 26 | Message string `xml:"Message" json:"message"` 27 | Resource string `xml:"Resource"` 28 | HostId string `xml:"HostId"` 29 | } 30 | 31 | func (err ObsError) Error() string { 32 | return fmt.Sprintf("obs: service returned error: Status=%s, Code=%s, Message=%s, RequestId=%s", 33 | err.Status, err.Code, err.Message, err.RequestId) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/obs/extension.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Huawei Technologies Co.,Ltd. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use 3 | // this file except in compliance with the License. You may obtain a copy of the 4 | // License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software distributed 9 | // under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | // CONDITIONS OF ANY KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations under the License. 12 | 13 | package obs 14 | 15 | import ( 16 | "fmt" 17 | "strings" 18 | ) 19 | 20 | type extensionOptions interface{} 21 | type extensionHeaders func(headers map[string][]string, isObs bool) error 22 | 23 | func setHeaderPrefix(key string, value string) extensionHeaders { 24 | return func(headers map[string][]string, isObs bool) error { 25 | if strings.TrimSpace(value) == "" { 26 | return fmt.Errorf("set header %s with empty value", key) 27 | } 28 | setHeaders(headers, key, []string{value}, isObs) 29 | return nil 30 | } 31 | } 32 | 33 | // WithReqPaymentHeader sets header for requester-pays 34 | func WithReqPaymentHeader(requester PayerType) extensionHeaders { 35 | return setHeaderPrefix(REQUEST_PAYER, string(requester)) 36 | } 37 | -------------------------------------------------------------------------------- /quickstart/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | minio: 5 | image: minio/minio:latest 6 | command: server /data --console-address=":9001" 7 | ports: 8 | - "9000:9000" 9 | - "9001:9001" 10 | volumes: 11 | - minio:/data 12 | environment: 13 | - MINIO_ROOT_USER=zpan 14 | - MINIO_ROOT_PASSWORD=zpanminio 15 | - MINIO_DOMAIN=minio.localhost 16 | 17 | zpan: 18 | image: saltbo/zpan:latest 19 | volumes: 20 | - zpcfg:/etc/zpan 21 | - zpdata:/srv # only for sqlite 22 | ports: 23 | - "8222:8222" 24 | depends_on: 25 | - minio 26 | 27 | volumes: 28 | minio: 29 | zpcfg: 30 | zpdata: 31 | -------------------------------------------------------------------------------- /web/dist/css/chunk-046e590c.c120f90d.css: -------------------------------------------------------------------------------- 1 | .file-card[data-v-7d28ab66]{width:800px;margin:0 auto;height:600px}.folder-card[data-v-7d28ab66]{min-width:800px;max-width:1200px;margin:0 auto;height:calc(100% - 120px)}.header .name[data-v-7d28ab66]{font-size:22px;font-weight:700}.header .time[data-v-7d28ab66]{font-size:12px;margin:10px 0}.time i[data-v-7d28ab66]{width:18px}.time span[data-v-7d28ab66]{margin-right:20px}.content[data-v-7d28ab66]{background:#f6f9fd;height:600px;text-align:center;padding-top:120px}.content i[data-v-7d28ab66]{font-size:90px}.content p[data-v-7d28ab66]{margin-top:30px} -------------------------------------------------------------------------------- /web/dist/css/chunk-0e8dbb5f.0d875ebb.css: -------------------------------------------------------------------------------- 1 | *{margin:0;padding:0}body,html{width:100%;height:100%}#app{font-family:'12px/1.5 "Microsoft YaHei", arial, SimSun, \5B8B\4F53;';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50;background-color:#f9f9f9;height:100%}.guest .el-card__header{text-align:center;border-bottom:none!important}.guest .el-card__header .title{font-size:20px;margin-top:10px}.guest .el-card__header .icon{font-size:30px;color:#f50057} -------------------------------------------------------------------------------- /web/dist/css/chunk-141f1d87.09f4af54.css: -------------------------------------------------------------------------------- 1 | .profile{width:60%} -------------------------------------------------------------------------------- /web/dist/css/chunk-14d2e418.95c79dda.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/web/dist/css/chunk-14d2e418.95c79dda.css -------------------------------------------------------------------------------- /web/dist/css/chunk-22dece4e.1d3af35b.css: -------------------------------------------------------------------------------- 1 | .tips{margin:0 10px;color:#979696}.tips i{padding:0 2px} -------------------------------------------------------------------------------- /web/dist/css/chunk-26cc1f8f.a0ccf9af.css: -------------------------------------------------------------------------------- 1 | .warp{background:#eef2f6!important;padding-top:100px}.box-card{width:500px;margin:0 auto}.header .name{font-weight:700;font-size:20px}.header .time{float:right;font-size:13px}.form{padding:10px 20px} -------------------------------------------------------------------------------- /web/dist/css/chunk-2bcfbabe.66bfbc46.css: -------------------------------------------------------------------------------- 1 | .copyright[data-v-6f8972c8]{text-align:center;position:absolute;bottom:20px;font-size:10px}.el-main[data-v-6f8972c8]{padding:10px!important}.el-aside .el-menu[data-v-6f8972c8]{border-right:1px solid #fff;font-weight:500;padding:0 10px}.el-aside .el-menu-item[data-v-6f8972c8]:focus,.el-aside .el-menu-item[data-v-6f8972c8]:hover{outline:0;background-color:#eaeaea!important} -------------------------------------------------------------------------------- /web/dist/css/chunk-4fae512a.09f4af54.css: -------------------------------------------------------------------------------- 1 | .profile{width:60%} -------------------------------------------------------------------------------- /web/dist/css/chunk-51b64701.09f4af54.css: -------------------------------------------------------------------------------- 1 | .profile{width:60%} -------------------------------------------------------------------------------- /web/dist/css/chunk-5db82f0c.1d3af35b.css: -------------------------------------------------------------------------------- 1 | .tips{margin:0 10px;color:#979696}.tips i{padding:0 2px} -------------------------------------------------------------------------------- /web/dist/css/chunk-6955f844.c4257cb3.css: -------------------------------------------------------------------------------- 1 | .toolbar[data-v-3916029e]{height:45px;border-bottom:1px solid #f2f6fd}.toolbar .tips[data-v-3916029e]{font-size:12px;color:#8a8989}.th[data-v-3916029e]{font-size:12px;color:#333;padding-top:5px}.matter-icon[data-v-3916029e]{font-size:22px}.matter-title[data-v-3916029e]{display:inline;margin-left:5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;top:-2px;position:relative}.el-table__row .operation[data-v-3916029e]{display:none}.el-table__row:hover .operation[data-v-3916029e]{display:block}.operation .el-link[data-v-3916029e]{font-size:20px!important;margin:0 2px} -------------------------------------------------------------------------------- /web/dist/css/chunk-77a33456.e0eb3850.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:pdf;src:url(../fonts/pdf.fde29d48.eot);src:url(../fonts/pdf.fde29d48.eot?#iefix) format("embedded-opentype"),url(../fonts/pdf.06fc6a29.woff2) format("woff2"),url(../fonts/pdf.7928efbe.woff) format("woff"),url(../fonts/pdf.8acc3f55.ttf) format("truetype"),url(../img/pdf.367cd90b.svg?#pdf) format("svg");font-weight:400;font-style:normal}.vue-pdf-app-icon:after,.vue-pdf-app-icon:before{font-family:pdf;font-size:1.1rem;display:inline;text-decoration:inherit;text-align:center;font-variant:normal;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.vue-pdf-app-icon.sidebar-toggle:before{content:"\F0C9"}.vue-pdf-app-icon.dropdown-toolbar-button:after{content:"\E810"}.vue-pdf-app-icon.secondary-toolbar-toggle:before{content:"\F142"}.vue-pdf-app-icon.find-previous:before{content:"\E813"}.vue-pdf-app-icon.find-next:before{content:"\E812"}.vue-pdf-app-icon.page-up:before{content:"\E801"}.vue-pdf-app-icon.page-down:before{content:"\E802"}.vue-pdf-app-icon.zoom-out:before{content:"\E820"}.vue-pdf-app-icon.zoom-in:before{content:"\E821"}.vue-pdf-app-icon.presentation-mode:before{content:"\F0B2"}.vue-pdf-app-icon.print-button:before{content:"\E81F"}.vue-pdf-app-icon.open-file:before{content:"\F115"}.vue-pdf-app-icon.download-button:before{content:"\E81C"}.vue-pdf-app-icon.bookmark-button:before{content:"\F097"}.vue-pdf-app-icon.view-thumbnail:before{content:"\E815"}.vue-pdf-app-icon.view-outline:before{content:"\E814"}.vue-pdf-app-icon.view-attachments:before{content:"\E807"}.vue-pdf-app-icon.view-find:before{content:"\E817"}.vue-pdf-app-icon.first-page:before{content:"\E830"}.vue-pdf-app-icon.last-page:before{content:"\E82F"}.vue-pdf-app-icon.rotate-clockwise:before{content:"\E829"}.vue-pdf-app-icon.rotate-counter-clockwise:before{content:"\E82A"}.vue-pdf-app-icon.select-tool:before{content:"\F246"}.vue-pdf-app-icon.hand-tool:before{content:"\F256"}.vue-pdf-app-icon.scroll-vertical:before{content:"\E836"}.vue-pdf-app-icon.scroll-horizontal:before{content:"\E835"}.vue-pdf-app-icon.scroll-wrapped:before{content:"\E816"}.vue-pdf-app-icon.spread-none:before{content:"\E831"}.vue-pdf-app-icon.spread-odd:before{content:"\E83D"}.vue-pdf-app-icon.spread-even:before{content:"\E83C"}.vue-pdf-app-icon.document-properties:before{content:"\F129"}html[dir=rtl] .vue-pdf-app-icon.find-previous:before{content:"\E812"}html[dir=rtl] .vue-pdf-app-icon.find-next:before{content:"\E813"}html[dir=rtl] .vue-pdf-app-icon.first-page:before{content:"\E82F"}html[dir=rtl] .vue-pdf-app-icon.last-page:before{content:"\E830"} -------------------------------------------------------------------------------- /web/dist/css/chunk-77b2d504.439841c9.css: -------------------------------------------------------------------------------- 1 | .th[data-v-59aea4fa]{font-size:12px;color:#333;padding-top:5px}.matter-icon[data-v-59aea4fa]{font-size:28px}.matter-title[data-v-59aea4fa]{display:inline;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;top:-4px;position:relative} -------------------------------------------------------------------------------- /web/dist/css/chunk-b66bdb3e.f01cebc8.css: -------------------------------------------------------------------------------- 1 | .copyright[data-v-2907b69d]{text-align:center;position:absolute;bottom:20px;font-size:10px}.el-main[data-v-2907b69d]{padding:10px!important}.el-aside .el-menu[data-v-2907b69d]{border-right:1px solid #fff;font-weight:500}.el-aside .el-menu-item[data-v-2907b69d]:focus,.el-aside .el-menu-item[data-v-2907b69d]:hover{outline:0;background-color:#eaeaea!important} -------------------------------------------------------------------------------- /web/dist/css/chunk-bde9bbc0.6a417862.css: -------------------------------------------------------------------------------- 1 | .el-drawer__body{overflow:auto}.footer{margin-left:142px;margin-top:25px} -------------------------------------------------------------------------------- /web/dist/css/chunk-c35cc142.1579c98c.css: -------------------------------------------------------------------------------- 1 | .installer{width:900px;margin:50px auto}.step{margin-bottom:80px}.form{width:600px;margin:0 auto} -------------------------------------------------------------------------------- /web/dist/css/chunk-e11c2bda.be282ce8.css: -------------------------------------------------------------------------------- 1 | .uploader .size{color:#878c9c}.uploader .speed{color:#06a7ff;float:right}.uploader .tip{color:#afb3bf;margin-top:10px;text-align:center}.uploader .matter-icon{font-size:35px;padding-left:5px}.el-header{display:-webkit-box;display:-ms-flexbox;display:flex;line-height:60px;background-color:#fff;-webkit-box-shadow:1px 1px 8px #c9c9c9;box-shadow:1px 1px 8px #c9c9c9;margin-bottom:5px}.el-header .logo{width:150px;display:inline-block;font-size:35px;padding:0 15px;vertical-align:middle}.logo img{cursor:pointer}.el-header .navbar{font-weight:700}.el-header .storage{margin:15px 0} -------------------------------------------------------------------------------- /web/dist/css/chunk-e3ab30f8.8ad46891.css: -------------------------------------------------------------------------------- 1 | .el-aside .el-menu[data-v-1df4bb25]{border-right:1px solid #fff;font-weight:500}.el-aside .el-menu-item[data-v-1df4bb25]:focus,.el-aside .el-menu-item[data-v-1df4bb25]:hover{outline:0;background-color:#eaeaea!important} -------------------------------------------------------------------------------- /web/dist/css/index.d455708c.css: -------------------------------------------------------------------------------- 1 | *{margin:0;padding:0}body,html{width:100%;height:100%;overflow:hidden}#app{font-family:'12px/1.5 "Microsoft YaHei", arial, SimSun, \5B8B\4F53;';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50;background-color:#fff;height:100%}.explorer[data-v-27e97df8]{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:10px}.explorer-item[data-v-27e97df8]{width:80px;padding:15px;text-align:center;cursor:pointer}.explorer-item[data-v-27e97df8]:hover{background:#f0f6fd;border-radius:5px}.explorer-item i[data-v-27e97df8]{font-size:55px}.explorer-item p[data-v-27e97df8]{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.matter-icon[data-v-b4e2310a]{font-size:35px}.matter-title[data-v-b4e2310a]{display:inline;margin-left:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;top:-8px;position:relative}.el-table__row .operation[data-v-b4e2310a]{display:none}.el-table__row:hover .operation[data-v-b4e2310a]{display:block}.el-table[data-v-b4e2310a] .el-checkbox__inner{width:15px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.operation .el-link[data-v-b4e2310a]{font-size:20px!important;margin:0 2px}@font-face{font-family:iconfont;src:url(//at.alicdn.com/t/font_2113109_cqmk143zx9t.woff2?t=1629042221534) format("woff2"),url(//at.alicdn.com/t/font_2113109_cqmk143zx9t.woff?t=1629042221534) format("woff"),url(//at.alicdn.com/t/font_2113109_cqmk143zx9t.ttf?t=1629042221534) format("truetype")}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-file:before{content:"\E98F";color:#c9c9c9}.icon-compressed-file:before{content:"\E61B";color:#f4c446}.icon-xml:before{content:"\E66E";color:#fc7b24}.icon-audio:before{content:"\E8A4";color:#379fd3}.icon-text:before{content:"\E60D";color:#f9ca06}.icon-video:before{content:"\E609";color:#8095ff}.icon-zip:before{content:"\E60B"}.icon-excel:before{content:"\E6D6";color:#107b0f}.icon-pdf:before{content:"\E64F";color:#dc2e1b}.icon-ppt:before{content:"\E642";color:#d24625}.icon-html:before{content:"\E667";color:#f7622c}.icon-psd:before{content:"\E66A"}.icon-rtf:before{content:"\E66B"}.icon-image:before{content:"\E606";color:#1296db}.icon-doc:before{content:"\E623";color:#0d47a1}.icon-grid:before{content:"\E6EF"}.icon-list:before{content:"\E67A"}.header[data-v-72354125]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row;flex-flow:row}.bread[data-v-72354125]{-webkit-box-flex:1;-ms-flex:1;flex:1}.loadtips[data-v-72354125]{width:200px;text-align:right;font-size:12px;color:#7c7c7c} -------------------------------------------------------------------------------- /web/dist/fonts/element-icons.535877f5.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/web/dist/fonts/element-icons.535877f5.woff -------------------------------------------------------------------------------- /web/dist/fonts/element-icons.732389de.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/web/dist/fonts/element-icons.732389de.ttf -------------------------------------------------------------------------------- /web/dist/fonts/pdf.06fc6a29.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/web/dist/fonts/pdf.06fc6a29.woff2 -------------------------------------------------------------------------------- /web/dist/fonts/pdf.7928efbe.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/web/dist/fonts/pdf.7928efbe.woff -------------------------------------------------------------------------------- /web/dist/fonts/pdf.8acc3f55.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/web/dist/fonts/pdf.8acc3f55.ttf -------------------------------------------------------------------------------- /web/dist/fonts/pdf.fde29d48.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saltbo/zpan/2a1f48ca61d0539cc0a7d4d5947ca16d56e17894/web/dist/fonts/pdf.fde29d48.eot -------------------------------------------------------------------------------- /web/dist/img/default-skin.b257fa9c.svg: -------------------------------------------------------------------------------- 1 | default-skin 2 -------------------------------------------------------------------------------- /web/dist/js/chunk-0e8dbb5f.9b6b84ce.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-0e8dbb5f"],{"05ad":function(n,t,e){},"793e":function(n,t,e){"use strict";e("05ad")},"9ed6":function(n,t,e){"use strict";e.r(t);var o=function(){var n=this,t=n.$createElement,e=n._self._c||t;return e("div",{attrs:{id:"app"}},[e("Topbar"),e("router-view")],1)},u=[],a=e("0b47"),c={components:{Topbar:a["a"]},data:function(){return{}},watch:{},methods:{},mounted:function(){}},r=c,i=(e("793e"),e("2877")),s=Object(i["a"])(r,o,u,!1,null,null,null);t["default"]=s.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-141f1d87.6d003a26.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-141f1d87"],{"2fff":function(r,e,s){"use strict";s.r(e);var o=function(){var r=this,e=r.$createElement,s=r._self._c||e;return s("el-card",{staticClass:"box-card",attrs:{shadow:"never"}},[s("div",{staticClass:"clearfix",attrs:{slot:"header"},slot:"header"},[s("span",[r._v("账户安全")])]),s("el-form",{ref:"form",staticClass:"profile",attrs:{model:r.form,rules:r.rules,size:"medium","label-width":"100px"}},[s("el-form-item",{attrs:{label:"旧密码",prop:"old_password"}},[s("el-input",{attrs:{type:"password"},model:{value:r.form.old_password,callback:function(e){r.$set(r.form,"old_password",e)},expression:"form.old_password"}})],1),s("el-form-item",{attrs:{label:"新密码",prop:"new_password"}},[s("el-input",{attrs:{type:"password"},model:{value:r.form.new_password,callback:function(e){r.$set(r.form,"new_password",e)},expression:"form.new_password"}})],1),s("el-form-item",{attrs:{label:"新密码确认",prop:"new_passwordr"}},[s("el-input",{attrs:{type:"password"},model:{value:r.form.new_passwordr,callback:function(e){r.$set(r.form,"new_passwordr",e)},expression:"form.new_passwordr"}})],1),s("el-form-item",[s("el-button",{attrs:{type:"primary"},on:{click:function(e){return r.submitForm("form")}}},[r._v("保存")])],1)],1)],1)},t=[],a=(s("7f7f"),{data:function(){var r=this,e=function(e,s,o){""===s?o(new Error("请输入密码")):(""!==r.form.new_passwordr&&r.$refs.form.validateField("new_passwordr"),o())},s=function(e,s,o){""===s?o(new Error("请再次输入密码")):s!==r.form.new_password?o(new Error("两次输入密码不一致!")):o()};return{form:{},rules:{old_password:[{trigger:"blur",required:!0,message:"请输入当前密码"}],new_password:[{trigger:"blur",required:!0,validator:e}],new_passwordr:[{trigger:"blur",required:!0,validator:s}]}}},methods:{submitForm:function(r){var e=this;this.$refs[r].validate((function(r){r&&e.$zpan.User.updatePassword(e.form).then((function(r){e.$message({type:"success",message:"修改成功!"})}))}))}},mounted:function(){this.$route.params}}),n=a,l=(s("f393"),s("2877")),i=Object(l["a"])(n,o,t,!1,null,null,null);e["default"]=i.exports},a802:function(r,e,s){},f393:function(r,e,s){"use strict";s("a802")}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-22dece4e.d90f78aa.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-22dece4e"],{"0a01":function(e,t,s){"use strict";s.r(t);var a=function(){var e=this,t=e.$createElement,s=e._self._c||t;return s("el-card",{attrs:{shadow:"never"}},[s("div",{staticClass:"clearfix",attrs:{slot:"header"},slot:"header"},[s("span",[e._v("发信邮箱设置")])]),s("el-form",{ref:"form",staticStyle:{width:"520px"},attrs:{model:e.form,"label-width":"100px"}},[s("el-form-item",{attrs:{label:"SMTP地址"}},[s("el-input",{model:{value:e.form.address,callback:function(t){e.$set(e.form,"address",t)},expression:"form.address"}})],1),s("el-form-item",{attrs:{label:"用户名"}},[s("el-input",{model:{value:e.form.username,callback:function(t){e.$set(e.form,"username",t)},expression:"form.username"}})],1),s("el-form-item",{attrs:{label:"密码"}},[s("el-input",{attrs:{type:"password"},model:{value:e.form.password,callback:function(t){e.$set(e.form,"password",t)},expression:"form.password"}})],1),s("el-form-item",{attrs:{label:"发信人"}},[s("el-input",{model:{value:e.form.sender,callback:function(t){e.$set(e.form,"sender",t)},expression:"form.sender"}})],1),s("el-form-item",{attrs:{label:"启用"}},[s("el-switch",{model:{value:e.form.enabled,callback:function(t){e.$set(e.form,"enabled",t)},expression:"form.enabled"}}),s("span",{staticClass:"tips"},[s("i",{staticClass:"el-icon-warning"}),e._v("开启后可以使用完整账户流程(邮箱验证,找回密码)")])],1),s("el-form-item",[s("el-button",{attrs:{type:"primary"},on:{click:e.onSubmit}},[e._v("保存")])],1)],1)],1)},r=[],o={data:function(){return{form:{}}},methods:{onSubmit:function(){var e=this;this.$zpan.System.optSave("core.email",this.form).then((function(t){e.$message({type:"success",message:e.$t("msg.save-success")})}))}},mounted:function(){var e=this;this.$zpan.System.optGet("core.email").then((function(t){console.log(t.data),t.data&&(e.form=t.data)}))}},n=o,l=(s("a9d0"),s("2877")),i=Object(l["a"])(n,a,r,!1,null,null,null);t["default"]=i.exports},"3f0c":function(e,t,s){},a9d0:function(e,t,s){"use strict";s("3f0c")}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-26cc1f8f.1fb61c16.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-26cc1f8f"],{"0a49":function(t,e,r){var a=r("9b43"),n=r("626a"),i=r("4bf8"),s=r("9def"),o=r("cd1c");t.exports=function(t,e){var r=1==t,c=2==t,u=3==t,d=4==t,f=6==t,l=5==t||f,h=e||o;return function(e,o,p){for(var w,m,v=i(e),b=n(v),y=a(o,p,3),x=s(b.length),_=0,$=r?h(e,x):c?h(e,0):void 0;x>_;_++)if((l||_ in b)&&(w=b[_],m=y(w,_,v),t))if(r)$[_]=m;else if(m)switch(t){case 3:return!0;case 5:return w;case 6:return _;case 2:$.push(w)}else if(d)return!1;return f?-1:u||d?d:$}}},1169:function(t,e,r){var a=r("2d95");t.exports=Array.isArray||function(t){return"Array"==a(t)}},5566:function(t,e,r){"use strict";r("e6e6")},7514:function(t,e,r){"use strict";var a=r("5ca1"),n=r("0a49")(5),i="find",s=!0;i in[]&&Array(1)[i]((function(){s=!1})),a(a.P+a.F*s,"Array",{find:function(t){return n(this,t,arguments.length>1?arguments[1]:void 0)}}),r("9c6c")(i)},bc5a:function(t,e,r){"use strict";r.r(e);var a=function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("div",{staticClass:"warp"},[r("el-card",{staticClass:"box-card",attrs:{shadow:"never"}},[r("div",{staticClass:"header clearfix",attrs:{slot:"header"},slot:"header"},[r("span",{staticClass:"name"},[t._v(t._s(t.info.name))]),r("span",{staticClass:"time"},[t._v("分享于"+t._s(t._f("moment")(t.info.created,"YYYY-MM-DD HH:hh")))])]),r("el-form",{staticClass:"form",attrs:{"label-position":"top","label-width":"80px"}},[r("el-form-item",{attrs:{label:t.$t("share.drawcode-placeholder")}},[r("el-input",{staticStyle:{width:"310px","margin-right":"10px"},attrs:{size:"medium",autofocus:""},model:{value:t.drawcode,callback:function(e){t.drawcode=e},expression:"drawcode"}}),r("el-button",{attrs:{type:"primary",size:"medium"},on:{click:function(e){return t.draw(t.info.alias)}}},[t._v(t._s(t.$t("share.drawfile")))])],1)],1)],1)],1)},n=[],i=(r("7f7f"),r("7514"),{data:function(){return{info:{},drawcode:""}},methods:{draw:function(t){var e=this;this.$zpan.Share.draw(t,this.drawcode).then((function(r){localStorage.setItem("zpan-share",t),e.$router.push({name:"share-info"})}))}},mounted:function(){var t=this,e=this.$route.params.alias;this.drawcode=this.$route.query.pwd,this.drawcode?this.draw(e):this.$zpan.Share.find(e).then((function(e){t.info=e.data,document.title="".concat(t.info.name," | Zpan")}))}}),s=i,o=(r("5566"),r("2877")),c=Object(o["a"])(s,a,n,!1,null,null,null);e["default"]=c.exports},cd1c:function(t,e,r){var a=r("e853");t.exports=function(t,e){return new(a(t))(e)}},e6e6:function(t,e,r){},e853:function(t,e,r){var a=r("d3f4"),n=r("1169"),i=r("2b4c")("species");t.exports=function(t){var e;return n(t)&&(e=t.constructor,"function"!=typeof e||e!==Array&&!n(e.prototype)||(e=void 0),a(e)&&(e=e[i],null===e&&(e=void 0))),void 0===e?Array:e}}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2bcfbabe.7115086a.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2bcfbabe"],{"7abe":function(t,e,n){"use strict";n.r(e);var i=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("section",[n("Topbar",{ref:"topbar",attrs:{menus:t.$store.state.storages,logined:""}}),n("el-container",{staticStyle:{height:"100%"}},[n("el-aside",{staticStyle:{height:"100%","background-color":"#f4f4f5"},attrs:{width:"200px"}},[n("el-menu",{attrs:{"default-active":t.leftMenuActive,"background-color":"#f4f4f5",router:""}},t._l(t.leftMenus,(function(e){return n("el-menu-item",{key:e.path,attrs:{index:e.path}},[n("i",{class:e.icon}),n("span",{attrs:{slot:"title"},slot:"title"},[t._v(t._s(e.title))])])})),1)],1),n("el-main",{staticStyle:{height:"calc(100% - 65px)"}},[n("router-view",{on:{"upload-action":t.onUploadClick,"audio-open":t.onAudioOpen}})],1)],1)],1)},c=[],o=(n("7f7f"),n("8bbf")),a=n.n(o),s=n("c0d6"),u=n("41cb"),r=n("0b47"),l=n("ebbf"),h={mixins:[l["a"]],components:{Topbar:r["a"]},beforeRouteEnter:function(t,e,n){a.a.zpan.Storage.list().then((function(e){var i=e.data.list;0!=i.length?"/"!=t.path?(s["a"].commit("storages",i),n()):u["a"].push({path:"/".concat(i[0].name)}):n({name:"storages"})}))},data:function(){return{}},computed:{currentBucket:function(){return this.$route.params.sname},leftMenuActive:function(){return this.$route.fullPath},leftMenus:function(){var t=[{path:"/".concat(this.currentBucket),icon:"el-icon-document",title:this.$t("leftnav.files")},{path:"/".concat(this.currentBucket,"?type=doc"),icon:"el-icon-xx",title:this.$t("leftnav.doc")},{path:"/".concat(this.currentBucket,"/pic"),icon:"el-icon-xx",title:this.$t("leftnav.image")},{path:"/".concat(this.currentBucket,"?type=audio"),icon:"el-icon-xx",title:this.$t("leftnav.audio")},{path:"/".concat(this.currentBucket,"?type=video"),icon:"el-icon-xx",title:this.$t("leftnav.video")}];if(1==this.cs.mode){var e=[{path:"/".concat(this.currentBucket,"/share"),icon:"el-icon-share",title:this.$t("leftnav.share")},{path:"/".concat(this.currentBucket,"/recyclebin"),icon:"el-icon-delete",title:this.$t("leftnav.recyclebin")}];t.push.apply(t,e)}return t}},watch:{$route:function(t,e){}},methods:{onUploadClick:function(t){this.$refs.topbar.uploadSelect(t)},onAudioOpen:function(t,e){this.$refs.topbar.AplayerOpen(t,e)}},mounted:function(){}},f=h,d=(n("f922"),n("2877")),p=Object(d["a"])(f,i,c,!1,null,"6f8972c8",null);e["default"]=p.exports},d941:function(t,e,n){},ebbf:function(t,e,n){"use strict";n.d(e,"a",(function(){return c})),n.d(e,"b",(function(){return a}));n("7f7f"),n("ac6a");var i={data:function(){return{cs:{}}},watch:{$route:function(t,e){this.setCs()}},computed:{},methods:{setCs:function(){var t=this;this.$store.state.storages.forEach((function(e){e.name==t.$route.params.sname&&(t.cs=e)}))},getSid:function(){return this.cs.id}},mounted:function(){this.setCs()}},c=i,o={data:function(){return{visible:!1}},watch:{visible:function(t){!t&&this.$destroy()}},mounted:function(){document.body.appendChild(this.$el),this.visible=!0},destroyed:function(){this.$el.parentNode.removeChild(this.$el)},methods:{open:function(){this.visible=!0},close:function(){this.visible=!1},completed:function(){this.$emit("completed")},finish:function(){this.close(),this.completed()}}},a=o},f922:function(t,e,n){"use strict";n("d941")}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2d0a4fde.a1892a24.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0a4fde"],{"0998":function(t,a,e){"use strict";e.r(a);var s=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("el-row",{attrs:{gutter:20}},[e("el-col",{attrs:{span:18}},[e("el-card",{staticClass:"box-card",attrs:{shadow:"never"}},[e("div",{staticClass:"clearfix",attrs:{slot:"header"},slot:"header"},[e("span",[t._v("欢迎")])]),e("div",[e("el-row",{attrs:{gutter:12}},[e("el-col",{attrs:{span:3,sm:5}},[e("el-card",{attrs:{shadow:"always"}},[e("el-link",{attrs:{type:"primary",icon:"el-icon-document",href:"https://zpan.space",target:"_blank"}},[t._v("文档")])],1)],1),e("el-col",{attrs:{span:3,sm:5}},[e("el-card",{attrs:{shadow:"always"}},[e("el-link",{attrs:{type:"primary",icon:"el-icon-files",href:"https://github.com/saltbo/zpan",target:"_blank"}},[t._v("源码")])],1)],1),e("el-col",{attrs:{span:3,sm:5}},[e("el-card",{attrs:{shadow:"always"}},[e("el-link",{attrs:{type:"primary",icon:"el-icon-s-comment",href:"https://github.com/saltbo/zpan/issues",target:"_blank"}},[t._v("社区")])],1)],1),e("el-col",{attrs:{span:3,sm:5}},[e("el-card",{attrs:{shadow:"always"}},[e("el-link",{attrs:{type:"primary",icon:"el-icon-coffee-cup",href:"https://github.com/sponsors/saltbo",target:"_blank"}},[t._v("捐赠")])],1)],1)],1),e("p",{staticStyle:{"margin-top":"20px","font-size":"14px","font-weight":"bold","font-style":"italic"}},[t._v("Tips: 管理后台还在逐步迭代中,欢迎到社区提问交流~")])],1)])],1),e("el-col",{attrs:{span:6}},[e("el-card",{staticClass:"box-card",attrs:{shadow:"never"}},[e("div",{staticClass:"clearfix",attrs:{slot:"header"},slot:"header"},[e("span",[t._v("产品动态")])]),e("el-timeline",t._l(t.activities,(function(a,s){return e("el-timeline-item",{key:s,attrs:{timestamp:a.timestamp}},[t._v("\n "+t._s(a.content)+"\n ")])})),1)],1)],1)],1)},n=[],l={data:function(){return{activities:[{content:"v1.6.0版本发布",timestamp:"2021-07-12"},{content:"v1.5.0版本发布",timestamp:"2020-02-14"},{content:"v1.4.1版本发布",timestamp:"2020-10-17"},{content:"v1.4.0版本发布",timestamp:"2020-10-13"},{content:"v1.3.0版本发布",timestamp:"2020-09-20"},{content:"v1.2.0版本发布,进行了大规模重构",timestamp:"2020-09-06"},{content:"v1.1.0版本发布",timestamp:"2019-10-24"},{content:"v1.0.0版本发布,第一个可用版本",timestamp:"2019-10-12"},{content:"项目创立,完成基础功能",timestamp:"2019-09-28"}]}}},r=l,i=e("2877"),o=Object(i["a"])(r,s,n,!1,null,null,null);a["default"]=o.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2d0afa39.c1ef3224.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0afa39"],{"0ed2":function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("div")},s=[],i=(a("28a5"),a("768b")),u={mounted:function(){var e=this,t=this.$route.params.token64,a=atob(t).split("|zplat|"),n=Object(i["a"])(a,2),s=n[0],u=n[1];this.$zpan.User.activate(s,u).then((function(t){e.$message({type:"success",message:"激活成功,请输入密码登录。"}),e.$router.push({name:"signin",params:{email:s}})}))}},c=u,r=a("2877"),o=Object(r["a"])(c,n,s,!1,null,null,null);t["default"]=o.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2d0bce73.4499f935.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0bce73"],{"2a5c":function(e,t,r){"use strict";r.r(t);var s=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"guest"},[r("el-row",{staticStyle:{height:"80px"}}),r("div",{staticStyle:{width:"400px",margin:"0 auto"}},[r("el-card",{staticClass:"box-card",staticStyle:{padding:"10px 20px"}},[r("div",{attrs:{slot:"header"},slot:"header"},[r("i",{staticClass:"icon el-icon-postcard"}),r("p",{staticClass:"title"},[e._v("用户注册")])]),r("el-form",{ref:"formItem",attrs:{model:e.formItem,rules:e.rules}},[r("el-form-item",{attrs:{prop:"email"}},[r("el-input",{attrs:{placeholder:"电子邮箱",autofocus:""},model:{value:e.formItem.email,callback:function(t){e.$set(e.formItem,"email",t)},expression:"formItem.email"}})],1),r("el-form-item",{attrs:{prop:"password"}},[r("el-input",{attrs:{type:"password",placeholder:"密码"},model:{value:e.formItem.password,callback:function(t){e.$set(e.formItem,"password",t)},expression:"formItem.password"}})],1),r("el-form-item",{attrs:{prop:"password2"}},[r("el-input",{attrs:{type:"password",placeholder:"密码确认"},model:{value:e.formItem.password2,callback:function(t){e.$set(e.formItem,"password2",t)},expression:"formItem.password2"}})],1),r("el-form-item",{attrs:{prop:"invitation"}},[r("el-input",{attrs:{placeholder:"邀请码"},model:{value:e.formItem.ticket,callback:function(t){e.$set(e.formItem,"ticket",t)},expression:"formItem.ticket"}})],1),r("el-form-item",[r("el-row",[r("el-button",{staticStyle:{width:"100%"},attrs:{type:"primary"},on:{click:function(t){return e.signUp("formItem")}}},[e._v("注册账号")])],1),r("el-row",[r("el-link",{attrs:{type:"primary",underline:!1},on:{click:function(t){return e.$router.push({name:"signin"})}}},[e._v("返回登录")])],1)],1)],1)],1)],1)],1)},a=[],o=(r("7f7f"),{data:function(){var e=this,t=function(t,r,s){""===r?s(new Error("请输入密码")):(""!==e.formItem.password2&&e.$refs.formItem.validateField("password2"),s())},r=function(t,r,s){""===r?s(new Error("请再次输入密码")):r!==e.formItem.password?s(new Error("两次输入密码不一致!")):s()};return{rules:{email:[{required:!0,message:"请输入邮箱地址",trigger:"blur"},{type:"email",message:"请输入正确的邮箱地址",trigger:["blur","change"]}],password:[{validator:t,trigger:"blur",required:!0}],password2:[{validator:r,trigger:"blur",required:!0}]},formItem:{}}},methods:{signUp:function(e){var t=this;this.$refs[e].validate((function(e){e&&t.$zpan.User.signup(t.formItem).then((function(e){t.$message({type:"success",message:"注册成功!"}),t.$router.push({name:"signin"})}))}))}}}),i=o,l=r("2877"),n=Object(l["a"])(i,s,a,!1,null,null,null);t["default"]=n.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2d0c5700.3d133d9f.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0c5700"],{"3ea9":function(e,t,r){"use strict";r.r(t);var s=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"guest"},[r("el-row",{staticStyle:{height:"80px"}}),r("div",{staticStyle:{width:"400px",margin:"0 auto"}},[r("el-card",{staticClass:"box-card",staticStyle:{padding:"10px 20px"}},[r("div",{attrs:{slot:"header"},slot:"header"},[r("i",{staticClass:"icon el-icon-key"}),r("p",{staticClass:"title"},[e._v("找回密码")])]),r("el-form",{ref:"resetForm",attrs:{model:e.formItem,rules:e.rules}},[r("el-form-item",{attrs:{prop:"email"}},[r("el-input",{attrs:{placeholder:"电子邮箱"},model:{value:e.formItem.email,callback:function(t){e.$set(e.formItem,"email",t)},expression:"formItem.email"}})],1),r("el-form-item",[r("el-row",[r("el-button",{staticStyle:{width:"100%"},attrs:{type:"primary"},on:{click:function(t){return e.reset("resetForm")}}},[e._v("发送密码重置邮件")])],1),r("el-row",[r("el-link",{attrs:{type:"primary",underline:!1},on:{click:function(t){return e.$router.push({name:"signin"})}}},[e._v("返回登录")])],1)],1)],1)],1)],1)],1)},a=[],i=(r("7f7f"),{data:function(){return{rules:{email:[{required:!0,message:"请输入邮箱地址",trigger:"blur"},{type:"email",message:"请输入正确的邮箱地址",trigger:["blur","change"]}]},formItem:{}}},methods:{reset:function(e){var t=this;this.$refs[e].validate((function(e){e&&t.$zpan.User.applyPasswordReset(t.formItem.email).then((function(e){t.$message({type:"success",message:"找回密码邮件发送成功!"}),t.$router.push({name:"signin"})}))}))}}}),l=i,n=r("2877"),o=Object(n["a"])(l,s,a,!1,null,null,null);t["default"]=o.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2d0d76a6.8e1a29a4.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0d76a6"],{"775e":function(n,e,s){"use strict";s.r(e);var t=function(){var n=this,e=n.$createElement,s=n._self._c||e;return s("div")},u=[],i={mounted:function(){var n=this;this.$zpan.User.signout().then((function(e){n.$message({type:"success",message:"登出成功,欢迎下次再来~"}),n.$router.push({name:"signin"})}))}},a=i,c=s("2877"),o=Object(c["a"])(a,t,u,!1,null,null,null);e["default"]=o.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2d0daeb3.58a4f15e.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0daeb3"],{"6e3a":function(e,t,r){"use strict";r.r(t);var i=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"guest"},[r("el-row",{staticStyle:{height:"80px"}}),r("div",{staticStyle:{width:"400px",margin:"0 auto"}},[r("el-card",{staticClass:"box-card",staticStyle:{padding:"10px 20px"}},[r("div",{attrs:{slot:"header"},slot:"header"},[r("i",{staticClass:"icon el-icon-lock"}),r("p",{staticClass:"title"},[e._v("用户登录")])]),r("el-form",{ref:"formItem",attrs:{model:e.formItem,rules:e.rules}},[r("el-form-item",{attrs:{prop:"email"}},[r("el-input",{attrs:{placeholder:"用户名或邮箱"},model:{value:e.formItem.email,callback:function(t){e.$set(e.formItem,"email",t)},expression:"formItem.email"}})],1),r("el-form-item",{attrs:{prop:"password"}},[r("el-input",{attrs:{type:"password",placeholder:"密码"},nativeOn:{keyup:function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"enter",13,t.key,"Enter")?null:e.signIn("formItem")}},model:{value:e.formItem.password,callback:function(t){e.$set(e.formItem,"password",t)},expression:"formItem.password"}})],1),r("el-form-item",[r("el-row",[r("el-button",{staticStyle:{width:"100%"},attrs:{type:"primary"},on:{click:function(t){return e.signIn("formItem")}}},[e._v("登录")])],1),r("el-row",[r("el-col",{attrs:{span:12}},[r("el-link",{attrs:{type:"primary",underline:!1},on:{click:function(t){return e.goto("reset_apply")}}},[e._v("忘记密码")])],1),r("el-col",{staticStyle:{"text-align":"right"},attrs:{span:12}},[r("el-link",{attrs:{type:"primary",underline:!1},on:{click:function(t){return e.goto("signup")}}},[e._v("注册账号")])],1)],1)],1)],1)],1)],1)],1)},n=[],o=(r("a481"),r("7f7f"),{data:function(){return{rules:{email:[{required:!0,message:"请输入邮箱地址",trigger:"blur"}]},redirect:"/",formItem:{email:""}}},methods:{goto:function(e){this.$router.push({name:e})},signIn:function(e){var t=this;this.$refs[e].validate((function(e){e&&t.$zpan.User.signin(t.formItem).then((function(e){location.replace(t.redirect)})).catch((function(e){console.log(e.response)}))}))}},mounted:function(){this.$route.query.redirect&&(this.redirect=this.$route.query.redirect),this.$route.params.email&&(this.formItem.email=this.$route.params.email)}}),a=o,s=r("2877"),l=Object(s["a"])(a,i,n,!1,null,null,null);t["default"]=l.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2d0e95df.0118b3f4.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0e95df"],{"8cdb":function(t,n,e){"use strict";e.r(n);var o=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("div",[e("Topbar"),e("el-empty",{attrs:{description:"页面不存在"}},[e("el-button",{attrs:{type:"primary"},on:{click:function(n){return t.$router.go(-1)}}},[t._v("返回首页")])],1)],1)},r=[],c=e("0b47"),u={components:{Topbar:c["a"]},mounted:function(){console.log(this.$route)}},i=u,l=e("2877"),s=Object(l["a"])(i,o,r,!1,null,null,null);n["default"]=s.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-2d207759.e88b37b6.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d207759"],{a12e:function(e,t,r){"use strict";r.r(t);var s=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"guest"},[r("el-row",{staticStyle:{height:"80px"}}),r("div",{staticStyle:{width:"400px",margin:"0 auto"}},[r("el-card",{staticClass:"box-card",staticStyle:{padding:"10px 20px"}},[r("div",{attrs:{slot:"header"},slot:"header"},[r("i",{staticClass:"icon el-icon-key"}),r("p",{staticClass:"title"},[e._v("找回密码")])]),r("el-form",{ref:"formItem",attrs:{model:e.formItem,rules:e.rules}},[r("el-form-item",{attrs:{prop:"email"}},[r("el-input",{attrs:{placeholder:"电子邮箱",readonly:""},model:{value:e.formItem.email,callback:function(t){e.$set(e.formItem,"email",t)},expression:"formItem.email"}})],1),r("el-form-item",{attrs:{prop:"password"}},[r("el-input",{attrs:{type:"password",placeholder:"密码"},model:{value:e.formItem.password,callback:function(t){e.$set(e.formItem,"password",t)},expression:"formItem.password"}})],1),r("el-form-item",{attrs:{prop:"password2"}},[r("el-input",{attrs:{type:"password",placeholder:"密码确认"},model:{value:e.formItem.password2,callback:function(t){e.$set(e.formItem,"password2",t)},expression:"formItem.password2"}})],1),r("el-form-item",[r("el-row",[r("el-button",{staticStyle:{width:"100%"},attrs:{type:"primary"},on:{click:function(t){return e.reset("formItem")}}},[e._v("重置密码")])],1),r("el-row",[r("el-link",{attrs:{type:"primary",underline:!1},on:{click:function(t){return e.$router.push({name:"signin"})}}},[e._v("返回登录")])],1)],1)],1)],1)],1)],1)},o=[],a=(r("28a5"),r("768b")),i=(r("7f7f"),{data:function(){var e=this,t=function(t,r,s){""===r?s(new Error("请输入密码")):(""!==e.formItem.password2&&e.$refs.formItem.validateField("password2"),s())},r=function(t,r,s){""===r?s(new Error("请再次输入密码")):r!==e.formItem.password?s(new Error("两次输入密码不一致!")):s()};return{rules:{password:[{validator:t,trigger:"blur",required:!0}],password2:[{validator:r,trigger:"blur",required:!0}]},formItem:{}}},methods:{reset:function(e){var t=this;this.$refs[e].validate((function(e){if(e){var r=t.formItem.email,s=t.formItem.token,o=t.formItem.password;t.$zpan.User.passwordReset(r,s,o).then((function(e){t.$message({type:"success",message:"密码重置成功!"}),t.$router.push({name:"signin"})}))}}))}},mounted:function(){var e=this.$route.params.token64,t=atob(e).split("|zplat|"),r=Object(a["a"])(t,2),s=r[0],o=r[1];this.formItem={email:s,token:o}}}),l=i,n=r("2877"),m=Object(n["a"])(l,s,o,!1,null,null,null);t["default"]=m.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-4fae512a.cb0504d5.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-4fae512a"],{"045e":function(e,l,t){"use strict";t("8271")},"31bb":function(e,l,t){"use strict";t.r(l);var o=function(){var e=this,l=e.$createElement,t=e._self._c||l;return t("el-card",{staticClass:"box-card",attrs:{shadow:"never"}},[t("div",{staticClass:"clearfix",attrs:{slot:"header"},slot:"header"},[t("span",[e._v("个人信息")])]),t("el-form",{ref:"form",staticClass:"profile",attrs:{model:e.profile,rules:e.rules,"label-width":"100px"}},[t("el-form-item",{attrs:{label:"邮箱",prop:"email"}},[t("el-input",{attrs:{disabled:""},model:{value:e.user.email,callback:function(l){e.$set(e.user,"email",l)},expression:"user.email"}})],1),t("el-form-item",{attrs:{label:"邀请码",prop:"ticket"}},[t("el-input",{attrs:{disabled:""},model:{value:e.user.ticket,callback:function(l){e.$set(e.user,"ticket",l)},expression:"user.ticket"}})],1),t("el-form-item",{attrs:{label:"昵称",prop:"nickname"}},[t("el-input",{model:{value:e.profile.nickname,callback:function(l){e.$set(e.profile,"nickname",l)},expression:"profile.nickname"}})],1),t("el-form-item",{attrs:{label:"个人介绍",prop:"bio"}},[t("el-input",{attrs:{type:"textarea"},model:{value:e.profile.bio,callback:function(l){e.$set(e.profile,"bio",l)},expression:"profile.bio"}})],1),t("el-form-item",{attrs:{label:"URL",prop:"url"}},[t("el-input",{model:{value:e.profile.url,callback:function(l){e.$set(e.profile,"url",l)},expression:"profile.url"}})],1),t("el-form-item",{attrs:{label:"公司",prop:"company"}},[t("el-input",{model:{value:e.profile.company,callback:function(l){e.$set(e.profile,"company",l)},expression:"profile.company"}})],1),t("el-form-item",{attrs:{label:"地址",prop:"location"}},[t("el-input",{model:{value:e.profile.location,callback:function(l){e.$set(e.profile,"location",l)},expression:"profile.location"}})],1),t("el-form-item",{attrs:{label:"语言",prop:"locale"}},[t("el-select",{model:{value:e.profile.locale,callback:function(l){e.$set(e.profile,"locale",l)},expression:"profile.locale"}},e._l(e.langs,(function(l){return t("el-option",{key:l.value,attrs:{value:l.value,label:l.label}},[e._v(e._s(l.label))])})),1)],1),t("el-form-item",[t("el-button",{attrs:{type:"primary"},on:{click:function(l){return e.submitForm("form")}}},[e._v("保存")])],1)],1)],1)},a=[],r=(t("7f7f"),{data:function(){return{user:{},profile:{},rules:{},langs:[{label:"中文",value:"zh-CN"},{label:"English",value:"en"}]}},methods:{loadInfo:function(){var e=this;this.$zpan.User.profileGet().then((function(l){e.user=l.data,e.profile=l.data.profile}))},submitForm:function(e){var l=this;this.$refs[e].validate((function(e){e&&l.$zpan.User.updateProfile(l.profile).then((function(e){l.$message({type:"success",message:"保存成功!"}),l.loadInfo()}))}))}},mounted:function(){this.loadInfo()}}),i=r,n=(t("045e"),t("2877")),s=Object(n["a"])(i,o,a,!1,null,null,null);l["default"]=s.exports},8271:function(e,l,t){}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-5db82f0c.e82b051a.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-5db82f0c"],{"5f32":function(e,t,a){},c6e4:function(e,t,a){"use strict";a.r(t);var r=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("el-card",{attrs:{shadow:"never"}},[a("div",{staticClass:"clearfix",attrs:{slot:"header"},slot:"header"},[a("span",[e._v("站点设置")])]),a("el-form",{ref:"form",staticStyle:{width:"500px"},attrs:{model:e.form,"label-width":"100px"}},[a("el-form-item",{attrs:{label:"站点名称"}},[a("el-input",{model:{value:e.form.name,callback:function(t){e.$set(e.form,"name",t)},expression:"form.name"}})],1),a("el-form-item",{attrs:{label:"站点描述"}},[a("el-input",{attrs:{type:"textarea"},model:{value:e.form.intro,callback:function(t){e.$set(e.form,"intro",t)},expression:"form.intro"}})],1),a("el-form-item",{attrs:{label:"默认语言"}},[a("el-select",{attrs:{placeholder:"请选择系统默认语言"},model:{value:e.form.locale,callback:function(t){e.$set(e.form,"locale",t)},expression:"form.locale"}},[a("el-option",{attrs:{label:"中文",value:"zh-CN"}}),a("el-option",{attrs:{label:"英语",value:"en"}})],1)],1),a("el-form-item",{attrs:{label:"邀请码注册"}},[a("el-switch",{model:{value:e.form.invite_required,callback:function(t){e.$set(e.form,"invite_required",t)},expression:"form.invite_required"}}),a("span",{staticClass:"tips"},[a("i",{staticClass:"el-icon-warning"}),e._v("开启后只允许通过邀请码进行注册")])],1),a("el-form-item",[a("el-button",{attrs:{type:"primary"},on:{click:e.onSubmit}},[e._v("保存")])],1)],1)],1)},s=[],o={data:function(){return{form:{}}},methods:{refresh:function(){var e=this;this.$zpan.System.optGet("core.site").then((function(t){t.data&&(e.form=t.data)}))},onSubmit:function(){var e=this;this.$zpan.System.optSave("core.site",this.form).then((function(t){e.refresh(),e.$message({type:"success",message:e.$t("msg.save-success")})}))}},mounted:function(){this.refresh()}},l=o,n=(a("fa2b"),a("2877")),i=Object(n["a"])(l,r,s,!1,null,null,null);t["default"]=i.exports},fa2b:function(e,t,a){"use strict";a("5f32")}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-77b2d504.cf0bdd9d.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-77b2d504"],{4922:function(t,e,n){},"8eb4":function(t,e,n){"use strict";n("b796")},b796:function(t,e,n){},dd46:function(t,e,n){"use strict";n("4922")},e238:function(t,e,n){"use strict";n.r(e);var a=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[n("el-row",{staticClass:"th"},[n("span",{staticClass:"title"},[t._v(t._s(t.$t("title.share")))]),n("span",{staticClass:"loadtips",staticStyle:{float:"right"}},[t._v(t._s(t.loadedtips))])]),n("el-table",{staticStyle:{width:"100%"},attrs:{data:t.rows,"expand-row-keys":t.expandRowKeys,"row-key":"id","highlight-current-row":""},on:{"current-change":t.onCurrentChange}},[n("el-table-column",{attrs:{type:"expand",width:"40"},scopedSlots:t._u([{key:"default",fn:function(e){return[n("p",[n("span",[t._v(t._s(t.$t("share.link"))+":")]),n("a",{attrs:{href:e.row.link,target:"_blank"}},[t._v(t._s(e.row.link))]),e.row.protected?n("span",{staticStyle:{"margin-left":"20px"}},[t._v(t._s(t.$t("share.drawcode"))+":"+t._s(e.row.secret))]):t._e()])]}}])}),n("el-table-column",{attrs:{prop:"name"},scopedSlots:t._u([{key:"default",fn:function(e){return[e.row.type?n("i",{staticClass:"matter-icon el-icon-document"}):n("i",{staticClass:"matter-icon el-icon-folder",staticStyle:{color:"#ffc402"}}),n("span",{staticClass:"matter-title"},[t._v(t._s(e.row.name))])]}}])}),n("el-table-column",{attrs:{prop:"created",label:t.$t("share.created")},scopedSlots:t._u([{key:"default",fn:function(e){return[t._v(t._s(t._f("moment")(e.row.created)))]}}])}),n("el-table-column",{attrs:{prop:"expired",label:t.$t("share.expired")},scopedSlots:t._u([{key:"default",fn:function(e){return[t.isForever(e.row.expire_at)?n("span",[t._v("永久有效")]):n("span",[t._v(t._s(t._f("moment")(e.row.expire_at)))])]}}])}),n("el-table-column",{attrs:{label:"操作"},scopedSlots:t._u([{key:"default",fn:function(e){return[n("el-button",{attrs:{size:"mini",type:"text"},on:{click:function(n){return t.onDelete(e.$index,e.row)}}},[t._v("取消分享")])]}}])})],1)],1)},s=[],o={data:function(){return{rows:[],total:0,expandRowKeys:[]}},computed:{loadedtips:function(){var t=this.rows.length;return t==this.total?"已全部加载,共".concat(this.total,"个"):"已加载".concat(t,"个,共").concat(this.total,"个")}},methods:{isForever:function(t){return new Date(t).getFullYear()-(new Date).getFullYear()>10},listRefresh:function(){var t=this,e=window.location.host;this.$zpan.Share.list().then((function(n){var a=n.data;t.rows=a.list.map((function(t){return t.link="http://".concat(e,"/s/").concat(t.alias),t})),t.total=a.total}))},onDelete:function(t,e){var n=this;this.$confirm(this.$t("tips.share-cancel"),this.$t("share.cancel"),{type:"warning",confirmButtonText:this.$t("op.confirm"),cancelButtonText:this.$t("op.cancel")}).then((function(){n.$zpan.Share.remove(e.alias).then((function(t){n.$message({type:"success",message:n.$t("msg.cancel-success")}),n.listRefresh()}))}))},onCurrentChange:function(t,e){this.expandRowKeys=[t.id]}},mounted:function(){this.listRefresh()}},r=o,i=(n("dd46"),n("8eb4"),n("2877")),c=Object(i["a"])(r,a,s,!1,null,"59aea4fa",null);e["default"]=c.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-b66bdb3e.ec92846a.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-b66bdb3e"],{"3acf":function(t,e,n){},"5c67":function(t,e,n){"use strict";n("3acf")},"61c2":function(t,e,n){"use strict";n.r(e);var i=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("section",[n("Topbar",{attrs:{menus:t.$store.state.storages,logined:""}}),n("el-container",{staticStyle:{height:"100%"}},[n("el-aside",{staticStyle:{height:"100%","background-color":"#f4f4f5"},attrs:{width:"200px"}},[n("el-menu",{attrs:{"default-active":t.leftMenuActive,"background-color":"#f4f4f5",router:""}},t._l(t.leftMenus,(function(e){return n("el-menu-item",{key:e.path,attrs:{index:e.path}},[n("i",{class:e.icon}),n("span",{attrs:{slot:"title"},slot:"title"},[t._v(t._s(e.title))])])})),1)],1),n("el-main",[n("router-view")],1)],1)],1)},o=[],c=n("8bbf"),a=n.n(c),s=n("c0d6"),r=n("0b47"),u={components:{Topbar:r["a"]},beforeRouteEnter:function(t,e,n){a.a.zpan.Storage.list().then((function(t){var e=t.data.list;s["a"].commit("storages",e),n()}))},data:function(){return{}},computed:{leftMenuActive:function(){return this.$route.fullPath},leftMenus:function(){return[{path:"/settings/profile",icon:"el-icon-user",title:"个人信息"},{path:"/settings/security",icon:"el-icon-lock",title:"密码修改"},{path:"/settings/developer",icon:"el-icon-cpu",title:"开发者设置"}]}},watch:{},methods:{},mounted:function(){}},l=u,f=(n("5c67"),n("2877")),p=Object(f["a"])(l,i,o,!1,null,"2907b69d",null);e["default"]=p.exports}}]); -------------------------------------------------------------------------------- /web/dist/js/chunk-e3ab30f8.78491f1a.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-e3ab30f8"],{2953:function(t,e,i){"use strict";i.r(e);var s=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("section",[i("Topbar",{attrs:{logined:""}}),i("el-container",{staticStyle:{height:"100%"}},[i("el-aside",{staticStyle:{height:"100%","background-color":"#f4f4f5"},attrs:{width:"200px"}},[i("el-menu",{attrs:{"default-active":t.routeFullPath,"background-color":"#f4f4f5",router:""}},[i("el-menu-item",{attrs:{index:"/admin/dashboard"}},[i("i",{staticClass:"el-icon-s-home"}),i("span",{attrs:{slot:"title"},slot:"title"},[t._v("首页")])]),i("el-menu-item",{attrs:{index:"/admin/storages"}},[i("i",{staticClass:"el-icon-files"}),i("span",{attrs:{slot:"title"},slot:"title"},[t._v("存储管理")])]),i("el-menu-item",{attrs:{index:"/admin/users"}},[i("i",{staticClass:"el-icon-user"}),i("span",{attrs:{slot:"title"},slot:"title"},[t._v("用户管理")])]),i("el-submenu",{attrs:{index:"/admin/settings"}},[i("template",{slot:"title"},[i("i",{staticClass:"el-icon-files"}),i("span",[t._v("系统设置")])]),i("el-menu-item",{attrs:{index:"/admin/settings"}},[t._v("站点设置")]),i("el-menu-item",{attrs:{index:"/admin/settings/email"}},[t._v("发信邮箱")])],2)],1)],1),i("el-main",[i("router-view")],1)],1)],1)},a=[],n=i("0b47"),l={components:{Topbar:n["a"]},data:function(){return{routeFullPath:"disk"}},watch:{$route:function(t,e){this.routeFullPath=t.fullPath}},mounted:function(){this.routeFullPath=this.$route.fullPath}},o=l,r=(i("c71b"),i("2877")),u=Object(r["a"])(o,s,a,!1,null,"1df4bb25",null);e["default"]=u.exports},aa05:function(t,e,i){},c71b:function(t,e,i){"use strict";i("aa05")}}]); -------------------------------------------------------------------------------- /web/efs.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "embed" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | //go:embed dist/* 11 | var embedFs embed.FS 12 | 13 | type FileSystem struct { 14 | efs http.FileSystem 15 | } 16 | 17 | func NewFS() *FileSystem { 18 | return &FileSystem{http.FS(embedFs)} 19 | } 20 | 21 | func (fs FileSystem) Open(name string) (http.File, error) { 22 | f, err := fs.efs.Open(filepath.Join("dist", name)) 23 | if os.IsNotExist(err) { 24 | return fs.efs.Open("dist/index.html") // SPA应用需要始终加载index.html 25 | } 26 | 27 | return f, err 28 | } 29 | -------------------------------------------------------------------------------- /web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-contrib/gzip" 7 | "github.com/gin-gonic/gin" 8 | "github.com/saltbo/gopkg/ginutil" 9 | ) 10 | 11 | func SetupRoutes(ge *gin.Engine) { 12 | staticRouter := ge.Group("/") 13 | staticRouter.Use(gzip.Gzip(gzip.DefaultCompression)) 14 | ginutil.SetupEmbedAssets(staticRouter, NewFS(), "/css", "/js", "/fonts") 15 | ge.NoRoute(func(c *gin.Context) { 16 | if strings.HasPrefix(c.Request.RequestURI, "/api") { 17 | return 18 | } 19 | 20 | c.FileFromFS(c.Request.URL.Path, NewFS()) 21 | }) 22 | } 23 | --------------------------------------------------------------------------------