├── .github └── workflows │ ├── ci_build.yml │ ├── ci_docker.yml │ ├── ci_docker_github.yml │ └── ci_github_binary.yml ├── .gitignore ├── Dockerfile ├── Dockerfile_default ├── LICENSE ├── README.md ├── cmd └── start │ └── main.go ├── configs ├── demo.yml ├── dev.yml ├── docker.yml └── lan.yml ├── docs ├── README_zh-cn.md ├── doc.md ├── imgs │ ├── desktop_2.png │ ├── desktop_3.png │ ├── mobile_2.jpeg │ ├── v0.11.0 │ │ ├── newlook.png │ │ └── screenshot.png │ ├── v0.4.20 │ │ ├── files_panel.png │ │ ├── files_panel_3.png │ │ ├── files_panel_op.png │ │ ├── management_1.png │ │ ├── management_2.png │ │ ├── panels.jpg │ │ ├── settings_1.png │ │ ├── sharing_dir_qr_code.png │ │ ├── sharing_panel.png │ │ └── site_addr_qr_code.png │ ├── v0.5.2 │ │ └── login.png │ ├── v0.5.4 │ │ ├── screens.jpg │ │ └── screens_2.jpg │ └── v0.9.1 │ │ └── quickshare_1920.gif ├── pull_request_template.md └── screenshots.md ├── go.mod ├── go.sum ├── package.json ├── scripts ├── build_be.sh ├── build_exec.sh ├── copy_js.sh └── copy_js_dev.sh ├── src ├── client │ ├── files.go │ ├── settings.go │ ├── users.go │ ├── utils.go │ └── web │ │ ├── .babelrc │ │ ├── .gitignore │ │ ├── build │ │ └── template │ │ │ ├── index.template.dev.html │ │ │ └── index.template.html │ │ ├── jest.setup.js │ │ ├── package.json │ │ ├── postcss.config.mjs │ │ ├── src │ │ ├── app.tsx │ │ ├── client │ │ │ ├── files.ts │ │ │ ├── files_mock.ts │ │ │ ├── index.ts │ │ │ ├── settings.ts │ │ │ ├── settings_mock.ts │ │ │ ├── users.ts │ │ │ └── users_mock.ts │ │ ├── common │ │ │ ├── controls.ts │ │ │ ├── cron.ts │ │ │ ├── env.ts │ │ │ ├── errors.ts │ │ │ ├── hotkeys.ts │ │ │ ├── localstorage.ts │ │ │ ├── log_error.ts │ │ │ └── utils.ts │ │ ├── components │ │ │ ├── __test__ │ │ │ │ ├── pane_login.test.tsx │ │ │ │ ├── pane_settings.test.tsx │ │ │ │ ├── panel_files.test.tsx │ │ │ │ ├── panel_sharings.test.tsx │ │ │ │ ├── panel_uploadings.test.tsx │ │ │ │ ├── state_mgr.test.tsx │ │ │ │ └── topbar.test.tsx │ │ │ ├── api.ts │ │ │ ├── control │ │ │ │ ├── btn_list.tsx │ │ │ │ └── tabs.tsx │ │ │ ├── core_state.ts │ │ │ ├── dialog_settings.tsx │ │ │ ├── layers.tsx │ │ │ ├── layout │ │ │ │ ├── card.tsx │ │ │ │ ├── columns.tsx │ │ │ │ ├── container.tsx │ │ │ │ ├── flexbox.tsx │ │ │ │ ├── flowgrid.tsx │ │ │ │ ├── rows.tsx │ │ │ │ ├── segments.tsx │ │ │ │ └── table.tsx │ │ │ ├── pane_admin.tsx │ │ │ ├── pane_login.tsx │ │ │ ├── pane_settings.tsx │ │ │ ├── panel_files.tsx │ │ │ ├── panel_sharings.tsx │ │ │ ├── panel_uploadings.tsx │ │ │ ├── root_frame.tsx │ │ │ ├── state_mgr.tsx │ │ │ ├── state_updater.ts │ │ │ ├── topbar.tsx │ │ │ └── visual │ │ │ │ ├── banner_notfound.tsx │ │ │ │ ├── colors.ts │ │ │ │ ├── icons.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── qrcode.tsx │ │ │ │ └── title.tsx │ │ ├── i18n │ │ │ ├── en_US.ts │ │ │ ├── msger.ts │ │ │ └── zh_CN.ts │ │ ├── style │ │ │ └── tailwind.css │ │ ├── test │ │ │ └── helpers.ts │ │ ├── typings │ │ │ └── custom.d.ts │ │ └── worker │ │ │ ├── __test__ │ │ │ ├── upload.worker.test.ts │ │ │ └── upload_mgr.test.ts │ │ │ ├── chunk_uploader.ts │ │ │ ├── interface.ts │ │ │ ├── upload.baseworker.ts │ │ │ ├── upload.bg.worker.ts │ │ │ ├── upload.fg.worker.ts │ │ │ ├── upload_mgr.ts │ │ │ └── uploader.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── webpack.app.dev.js │ │ ├── webpack.app.prod.js │ │ ├── webpack.common.js │ │ ├── webpack.dev.js │ │ └── webpack.prod.js ├── cron │ └── wrapper.go ├── cryptoutil │ ├── cryptoutil_interface.go │ └── jwt │ │ └── jwt.go ├── db │ ├── common.go │ ├── interfaces.go │ ├── rdb │ │ ├── base │ │ │ ├── configs.go │ │ │ ├── files.go │ │ │ ├── files_sharings.go │ │ │ ├── files_uploadings.go │ │ │ ├── init.go │ │ │ └── users.go │ │ ├── sqlite │ │ │ ├── configs.go │ │ │ ├── files.go │ │ │ ├── files_sharings.go │ │ │ ├── files_uploadings.go │ │ │ ├── init.go │ │ │ └── users.go │ │ └── sqlitecgo │ │ │ ├── configs.go │ │ │ ├── files.go │ │ │ ├── files_sharings.go │ │ │ ├── files_uploadings.go │ │ │ ├── init.go │ │ │ └── users.go │ └── tests │ │ ├── common_test.go │ │ ├── config_test.go │ │ ├── files_test.go │ │ └── users_test.go ├── depidx │ └── deps.go ├── downloadmgr │ └── mgr.go ├── fs │ ├── fs_interface.go │ ├── local │ │ └── fs.go │ └── mem │ │ └── fs.go ├── golimiter │ ├── limiter.go │ └── limiter_test.go ├── handlers │ ├── fileshdr │ │ ├── async_handlers.go │ │ ├── handlers.go │ │ └── init.go │ ├── multiusers │ │ ├── async_handlers.go │ │ ├── captcha.go │ │ ├── handlers.go │ │ └── middlewares.go │ ├── settings │ │ └── handlers.go │ └── util.go ├── idgen │ ├── idgen_interface.go │ └── simpleidgen │ │ └── simple_id_gen.go ├── iolimiter │ └── iolimiter.go ├── kvstore │ ├── boltdbpvd │ │ ├── interface.go │ │ └── provider.go │ ├── kvstore_interface.go │ ├── memstore │ │ └── provider.go │ └── test │ │ └── provider_test.go ├── search │ └── fileindex │ │ ├── fsearch.go │ │ └── fsearch_test.go ├── server │ ├── config.go │ ├── config_load.go │ ├── config_load_test.go │ ├── init_deps.go │ ├── init_handlers.go │ ├── init_test.go │ ├── server.go │ ├── server_bench_test.go │ ├── server_concurrency_test.go │ ├── server_files_test.go │ ├── server_permission_test.go │ ├── server_settings_test.go │ ├── server_space_limit_test.go │ ├── server_users_test.go │ ├── test_helpers.go │ └── testdata │ │ ├── config_1.yml │ │ ├── config_4.yml │ │ ├── config_partial_db.yml │ │ ├── config_partial_users.yml │ │ └── quickshare.sqlite └── worker │ ├── interface.go │ └── localworker │ ├── worker.go │ └── worker_test.go ├── static ├── fs.go └── public │ ├── css │ ├── colors.css │ ├── dark.css │ ├── default.css │ ├── reset.css │ └── white.css │ ├── font │ └── Hurricane-Regular.ttf │ ├── img │ ├── favicon.png │ ├── favicon.svg │ ├── prism.png │ ├── px_by_Gre3g.webp │ ├── textured_paper.png │ └── tweed.webp │ └── manifest.json └── yarn.lock /.github/workflows/ci_build.yml: -------------------------------------------------------------------------------- 1 | name: ci-quickshare 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: 8 | - main 9 | - dev-** 10 | pull_request: 11 | branches: [main] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | 28 | # Install runtimes 29 | - uses: actions/setup-go@v2 30 | with: 31 | go-version: "^1.23.0" 32 | - uses: actions/setup-node@v2 33 | with: 34 | node-version: "18" 35 | - run: go version 36 | - name: Install dependencies 37 | run: | 38 | yarn 39 | - name: Backend tests 40 | run: | 41 | go test ./... 42 | - name: Frontend tests 43 | run: | 44 | cd ./src/client/web 45 | yarn test 46 | - name: Build 47 | run: | 48 | yarn build 49 | -------------------------------------------------------------------------------- /.github/workflows/ci_docker.yml: -------------------------------------------------------------------------------- 1 | name: ci-docker 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: {} 7 | # push: 8 | # branches: 9 | # - "dev-docker" 10 | 11 | jobs: 12 | push_to_registry: 13 | name: Push Docker image to Docker Hub 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v3 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | - name: Login to Docker Hub 23 | uses: docker/login-action@v3 24 | with: 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | - name: Set version 28 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 29 | - name: Build and push to Docker Hub with version tag 30 | uses: docker/build-push-action@v6 31 | with: 32 | context: . 33 | platforms: linux/amd64, linux/arm64 34 | file: ./Dockerfile_default 35 | push: true 36 | tags: hexxa/quickshare:${{ env.RELEASE_VERSION }} 37 | - name: Build and push to Docker Hub with latest tag 38 | uses: docker/build-push-action@v2 39 | with: 40 | context: . 41 | platforms: linux/amd64, linux/arm64 42 | file: ./Dockerfile_default 43 | push: true 44 | tags: hexxa/quickshare:latest 45 | -------------------------------------------------------------------------------- /.github/workflows/ci_docker_github.yml: -------------------------------------------------------------------------------- 1 | name: ci-docker-test 2 | 3 | on: 4 | # release: 5 | # types: [published] 6 | # workflow_dispatch: {} 7 | push: 8 | branches: 9 | - "dev-docker" 10 | 11 | env: 12 | REGISTRY: ghcr.io 13 | IMAGE_NAME: ${{ github.repository }} 14 | 15 | jobs: 16 | push_to_registry: 17 | name: Push Docker image to Docker Hub 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v3 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v3 26 | - name: Login to Docker Hub 27 | uses: docker/login-action@v3 28 | with: 29 | registry: ${{ env.REGISTRY }} 30 | username: ihexxa 31 | password: ${{ secrets.GHCR_PWD }} 32 | - name: Set version 33 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 34 | - name: Extract metadata (tags, labels) for Docker 35 | id: meta 36 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 37 | with: 38 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 39 | - name: Build and push to Docker Hub with version tag 40 | uses: docker/build-push-action@v6 41 | with: 42 | context: . 43 | platforms: linux/amd64, linux/arm64 44 | file: ./Dockerfile_default 45 | push: true 46 | tags: ghcr.io/ihexxa/quickshare:${{ env.RELEASE_VERSION }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | - name: Build and push to Docker Hub with version tag 49 | uses: docker/build-push-action@v6 50 | with: 51 | context: . 52 | platforms: linux/amd64, linux/arm64 53 | file: ./Dockerfile_default 54 | push: true 55 | tags: ghcr.io/ihexxa/quickshare:latest 56 | labels: ${{ steps.meta.outputs.labels }} 57 | # - name: Generate artifact attestation 58 | # uses: actions/attest-build-provenance@v2 59 | # with: 60 | # subject-name: ghcr.io/ihexxa/quickshare:${{ env.RELEASE_VERSION }} 61 | # subject-digest: ${{ steps.push.outputs.digest }} 62 | # push-to-registry: true 63 | -------------------------------------------------------------------------------- /.github/workflows/ci_github_binary.yml: -------------------------------------------------------------------------------- 1 | name: ci-binary 2 | 3 | # Controls when the action will run. 4 | on: 5 | release: 6 | types: [published] 7 | workflow_dispatch: {} 8 | 9 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 10 | jobs: 11 | # This workflow contains a single job called "build" 12 | build: 13 | # The type of runner that the job will run on 14 | runs-on: ubuntu-latest 15 | 16 | # Steps represent a sequence of tasks that will be executed as part of the job 17 | steps: 18 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-go@v2 # Install runtimes 21 | with: 22 | go-version: "^1.17.0" 23 | - uses: actions/setup-node@v2 24 | with: 25 | node-version: "12" 26 | - run: go version 27 | - name: Install dependencies 28 | run: | 29 | yarn 30 | - name: Build 31 | run: | 32 | yarn build 33 | - name: Upload artifacts 34 | uses: xresloader/upload-to-github-release@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | file: "dist/*" 39 | tags: true 40 | draft: false 41 | overwrite: true 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # frontend 2 | **/*.bundle.js 3 | **/*.js.map 4 | **/coverage 5 | **/files 6 | **/node_modules/* 7 | **/yarn-error 8 | **/static/public/js/*.js 9 | **/static/public/index.html 10 | **/**/*.d.ts.map 11 | 12 | # backend 13 | **/*/quickshare.db 14 | **/*/files/ 15 | **/*/uploadings/ 16 | 17 | # build 18 | dist 19 | tmp 20 | quickshare 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts AS build-fe 2 | ADD . /quickshare 3 | WORKDIR /quickshare 4 | RUN yarn run build:fe 5 | 6 | FROM golang:1.18 AS build-be 7 | COPY --from=build-fe /quickshare /quickshare 8 | WORKDIR /quickshare 9 | RUN /quickshare/scripts/build_exec.sh 10 | 11 | FROM debian:stable-slim 12 | RUN groupadd -g 8686 quickshare 13 | RUN useradd quickshare -u 8686 -g 8686 -m -s /bin/bash 14 | RUN usermod -a -G quickshare root 15 | COPY --from=build-be /quickshare/dist/quickshare /quickshare 16 | ADD configs/demo.yml /quickshare 17 | RUN mkdir -p /quickshare/root 18 | RUN chgrp -R quickshare /quickshare 19 | RUN chmod -R 0770 /quickshare 20 | CMD ["/quickshare/start", "-c", "/quickshare/demo.yml"] 21 | -------------------------------------------------------------------------------- /Dockerfile_default: -------------------------------------------------------------------------------- 1 | FROM node:lts AS build-fe 2 | ADD . /quickshare 3 | WORKDIR /quickshare 4 | RUN yarn run build:fe 5 | 6 | FROM golang:1.23 AS build-be 7 | COPY --from=build-fe /quickshare /quickshare 8 | WORKDIR /quickshare 9 | RUN `which go` build -o /quickshare/dist/quickshare/start /quickshare/cmd/start 10 | 11 | FROM debian:stable-slim 12 | RUN groupadd -g 8686 quickshare 13 | RUN useradd quickshare -u 8686 -g 8686 -m -s /bin/bash 14 | RUN usermod -a -G quickshare root 15 | COPY --from=build-be /quickshare/dist/quickshare /quickshare 16 | ADD configs/docker.yml /quickshare 17 | RUN mkdir -p /quickshare/root 18 | RUN chgrp -R quickshare /quickshare 19 | RUN chmod -R 0770 /quickshare 20 | CMD ["/quickshare/start", "-c", "/quickshare/docker.yml"] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Quickshare 3 |

4 |

5 | Quick and simple file sharing between different devices. 6 | ( 7 | Screenshots |  8 | Demo 9 | ) 10 |

11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

25 | 26 | 27 | 28 | 29 | English | [简体中文](./docs/README_zh-cn.md) 30 | 31 | > Quickshare is still under active development, please keep in mind that full backward compatibility is not guaranteed. 32 | 33 | ## Features 34 | 35 | - File Management 36 | - Support uploading, downloading, creating, deleting and moving files and folders 37 | - Support fuzzy searching files and folders in seconds 38 | - Resumable uploading and downloading 39 | - Manage files and folders in browser 40 | - Share directories to others, including anonymous 41 | - Scan QR codes to visit sharing folders 42 | - Upload hundreds of files at once 43 | - Steaming uploading: make it work behind CDN or reverse proxy 44 | - Files can also be managed from OS 45 | - User Management 46 | - Support multiple users 47 | - Each user has a role (user/admin) 48 | - User home directory 49 | - Per-user download & upload speed limiting 50 | - Per-user space quota 51 | - MISC 52 | - Adaptive UI 53 | - I18n support 54 | - Wallpaper customization 55 | - Cross-platform: support Linux, Mac and Windows 56 | 57 | ## Quick Start 58 | 59 | ### Run in Docker (Recommended) 60 | 61 | Following will start a `quickshare` docker and listen to `8686` port. 62 | 63 | Then you can open `http://127.0.0.1:8686` and log in with user name `qs` and password `1234`: 64 | 65 | ``` 66 | docker run \ 67 | --name quickshare \ 68 | -d -p 8686:8686 \ 69 | -v `pwd`/quickshare/root:/quickshare/root \ 70 | -e DEFAULTADMIN=qs \ 71 | -e DEFAULTADMINPWD=1234 \ 72 | hexxa/quickshare 73 | ``` 74 | 75 | - `DEFAULTADMIN` is the default user name 76 | - `DEFAULTADMINPWD` is the default user password 77 | - `/quickshare/root` is where the Quickshare stores files and directories. 78 | - Please refer to [this doc](./docs/doc.md) if you want to manage files and folders from OS. 79 | 80 | ### Run from source code 81 | 82 | Before start, please confirm that Go/Golang (>=1.17), Node.js and Yarn are installed on your machine. 83 | 84 | ``` 85 | # clone this repo 86 | git clone git@github.com:ihexxa/quickshare.git 87 | 88 | # go to repo's folder 89 | cd quickshare 90 | 91 | DEFAULTADMIN=qs DEFAULTADMINPWD=1234 yarn start 92 | ``` 93 | 94 | OK! Open `http://127.0.0.1:8686` in browser, and log in with user name `qs` and password `1234`. 95 | 96 | ### Run executable file 97 | 98 | - **Downloading**: Download last distribution(s) in [Release Page](https://github.com/ihexxa/quickshare/releases). 99 | - **Unzipping**: Unzip it and run following command `DEFAULTADMIN=qs DEFAULTADMINPWD=1234 ./quickshare`. (You may update its execution permission: e.g. run `chmod u+x quickshare`) 100 | - **Accessing**: At last, open `http://127.0.0.1:8686` in browser, and log in with user name `qs` and password `1234`. 101 | 102 | ### FAQ 103 | 104 | Coming soon. 105 | -------------------------------------------------------------------------------- /cmd/start/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | goflags "github.com/jessevdk/go-flags" 9 | 10 | serverPkg "github.com/ihexxa/quickshare/src/server" 11 | ) 12 | 13 | var args = &serverPkg.Args{} 14 | 15 | func main() { 16 | _, err := goflags.Parse(args) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | ctx := context.TODO() 22 | cfg, err := serverPkg.LoadCfg(ctx, args) 23 | if err != nil { 24 | fmt.Printf("failed to load config: %s", err) 25 | os.Exit(1) 26 | } 27 | 28 | srv, err := serverPkg.NewServer(cfg) 29 | if err != nil { 30 | fmt.Printf("failed to new server: %s", err) 31 | os.Exit(1) 32 | } 33 | 34 | err = srv.Start() 35 | if err != nil { 36 | fmt.Printf("failed to start server: %s", err) 37 | os.Exit(1) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /configs/demo.yml: -------------------------------------------------------------------------------- 1 | fs: 2 | root: "/quickshare/root" 3 | opensLimit: 1024 4 | openTTL: 60 # 1 min 5 | publicPath: "/quickshare/static/public" 6 | searchResultLimit: 16 7 | initFileIndex: true 8 | server: 9 | debug: false 10 | host: "0.0.0.0" 11 | port: 8686 12 | readTimeout: 2000 13 | writeTimeout: 86400000 # 1 day 14 | maxHeaderBytes: 512 15 | users: 16 | enableAuth: true 17 | defaultAdmin: "" 18 | defaultAdminPwd: "" 19 | cookieTTL: 604800 # 1 week 20 | cookieSecure: false 21 | cookieHttpOnly: true 22 | minUserNameLen: 3 23 | minPwdLen: 6 24 | captchaWidth: 256 25 | captchaHeight: 60 26 | captchaEnabled: true 27 | uploadSpeedLimit: 524288 # 500k/limiterCyc 28 | downloadSpeedLimit: 524288 # 500k/limiterCyc 29 | spaceLimit: 104857600 # 100MB 30 | limiterCapacity: 1000 31 | limiterCyc: 1000 # 1s 32 | predefinedUsers: 33 | - name: "demo" 34 | pwd: "Quicksh@re" 35 | role: "user" 36 | db: 37 | dbPath: "/quickshare/root/quickshare.sqlite" 38 | -------------------------------------------------------------------------------- /configs/dev.yml: -------------------------------------------------------------------------------- 1 | fs: 2 | root: "tmp" 3 | opensLimit: 1024 4 | openTTL: 60 # 1 min 5 | publicPath: "static/public" 6 | searchResultLimit: 16 7 | initFileIndex: true 8 | secrets: 9 | tokenSecret: "" 10 | server: 11 | debug: true 12 | host: "127.0.0.1" 13 | port: 8686 14 | readTimeout: 2000 15 | writeTimeout: 86400000 # 1 day 16 | maxHeaderBytes: 512 17 | dynamic: 18 | clientCfg: 19 | siteName: "Quickshare" 20 | siteDesc: "Quick and simple file sharing" 21 | bg: 22 | url: "" 23 | repeat: "repeat" 24 | position: "center" 25 | align: "fixed" 26 | users: 27 | enableAuth: true 28 | defaultAdmin: "" 29 | defaultAdminPwd: "" 30 | cookieTTL: 604800 # 1 week 31 | cookieSecure: false 32 | cookieHttpOnly: true 33 | minUserNameLen: 2 34 | minPwdLen: 4 35 | captchaWidth: 256 36 | captchaHeight: 60 37 | captchaEnabled: true 38 | uploadSpeedLimit: 524288 # 500KB/limiterCyc 39 | downloadSpeedLimit: 524288 # 500KB/limiterCyc 40 | spaceLimit: 104857600 # 100MB 41 | limiterCapacity: 1000 42 | limiterCyc: 1000 # 1s 43 | predefinedUsers: 44 | - name: "demo" 45 | pwd: "Quicksh@re" 46 | role: "user" 47 | workers: 48 | queueSize: 1024 49 | sleepCyc: 1 # in second 50 | workerCount: 2 51 | db: 52 | dbPath: "quickshare.sqlite" 53 | -------------------------------------------------------------------------------- /configs/docker.yml: -------------------------------------------------------------------------------- 1 | fs: 2 | root: "/quickshare/root" 3 | opensLimit: 1024 4 | openTTL: 60 # 1 min 5 | publicPath: "/quickshare/static/public" 6 | searchResultLimit: 16 7 | initFileIndex: true 8 | server: 9 | debug: false 10 | host: "0.0.0.0" 11 | port: 8686 12 | readTimeout: 2000 13 | writeTimeout: 86400000 # 1 day 14 | maxHeaderBytes: 512 15 | db: 16 | dbPath: "/quickshare/root/quickshare.sqlite" 17 | -------------------------------------------------------------------------------- /configs/lan.yml: -------------------------------------------------------------------------------- 1 | fs: 2 | root: "tmp" 3 | opensLimit: 1024 4 | openTTL: 60 # 1 min 5 | publicPath: "static/public" 6 | searchResultLimit: 16 7 | initFileIndex: true 8 | secrets: 9 | tokenSecret: "" 10 | server: 11 | debug: false 12 | host: "0.0.0.0" 13 | port: 8686 14 | readTimeout: 2000 15 | writeTimeout: 86400000 # 1 day 16 | maxHeaderBytes: 512 17 | dynamic: 18 | clientCfg: 19 | siteName: "Quickshare" 20 | siteDesc: "Quick and simple file sharing" 21 | bg: 22 | url: "" 23 | repeat: "repeat" 24 | position: "center" 25 | align: "fixed" 26 | users: 27 | enableAuth: true 28 | defaultAdmin: "" 29 | defaultAdminPwd: "" 30 | cookieTTL: 604800 # 1 week 31 | cookieSecure: false 32 | cookieHttpOnly: true 33 | minUserNameLen: 2 34 | minPwdLen: 4 35 | captchaWidth: 256 36 | captchaHeight: 60 37 | captchaEnabled: true 38 | uploadSpeedLimit: 524288 # 500KB/limiterCyc 39 | downloadSpeedLimit: 524288 # 500KB/limiterCyc 40 | spaceLimit: 104857600 # 100MB 41 | limiterCapacity: 1000 42 | limiterCyc: 1000 # 1s 43 | workers: 44 | queueSize: 1024 45 | sleepCyc: 1 # in second 46 | workerCount: 2 47 | db: 48 | dbPath: "/tmp/quickshare.sqlite" 49 | -------------------------------------------------------------------------------- /docs/README_zh-cn.md: -------------------------------------------------------------------------------- 1 |

2 | Quickshare 3 |

4 |

5 | 简单的文件共享服务, 使用Go/Golang, Typescript, Gin, React, Boltdb等构建. 6 | ( 7 | 界面截图 |  8 | 录频 9 | ) 10 |

11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

25 | 26 | 27 | 28 | [English](../README.md) | 简体中文 29 | 30 | > Quickshare 仍然活跃开发中, 请意识到目前每个版本并不保证向前兼容. 31 | 32 | ## 主要功能 33 | 34 | - 文件管理 35 | - 支持上传,下载,创建,删除和移动文件和文件夹 36 | - 支持文件与目录模糊查找,在几秒内返回结果 37 | - 可恢复的上传和下载(断点续传) 38 | - 在浏览器中管理文件和文件夹 39 | - 将目录共享他人,包括未登录用户 40 | - 通过扫一扫访问共享文件夹/网站 41 | - 一次上传上百个文件 42 | - 流式上传: 使它可工作在 CDN 或 反向代理 之后 43 | - 也可通过操作系统管理文件 44 | - 用户管理 45 | - 支持多用户 46 | - 每个用户有个各自角色(user/admin) 47 | - 用户 home 目录 48 | - 用户级别的上传下载速度限制 49 | - 用户级别的空间限制 50 | - 其他 51 | - 自适应 UI 52 | - 多语言支持 53 | - 自定义壁纸支持 54 | - 跨平台: 支持 Linux, Mac and Windows 55 | 56 | ## 快速开始 57 | 58 | ### 通过 Docker 运行 (推荐) 59 | 60 | 下面会启动一个 `quickshare` docker 并监听 `8686` 端口. 61 | 62 | 然后你可以打开 `http://127.0.0.1:8686` 并且使用用户名 `qs` 和 密码 `1234` 登入. 63 | 64 | ``` 65 | docker run \ 66 | --name quickshare \ 67 | -d -p 8686:8686 \ 68 | -v `pwd`/quickshare/root:/quickshare/root \ 69 | -e DEFAULTADMIN=qs \ 70 | -e DEFAULTADMINPWD=1234 \ 71 | hexxa/quickshare 72 | ``` 73 | 74 | - `DEFAULTADMIN` 是默认的用户名 75 | - `DEFAULTADMINPWD` 是默认的用户密码 76 | - `/quickshare/root` 是 Quickshare 保存文件和目录的地方 77 | - 如果你想同时在操作系统管理文件和文件夹,请参考 [这个文档](./docs/doc.md) 78 | 79 | ### 运行源代码 80 | 81 | 在开始之前, 请确认 Go/Golang (>=1.17), Node.js 和 Yarn 已经安装在您的机器. 82 | 83 | ``` 84 | # clone this repo 85 | git clone git@github.com:ihexxa/quickshare.git 86 | 87 | # go to repo's folder 88 | cd quickshare 89 | 90 | DEFAULTADMIN=qs DEFAULTADMINPWD=1234 yarn start 91 | ``` 92 | 93 | OK! 在浏览器中打开 `http://127.0.0.1:8686`, 并且使用用户名 `qs` 和 密码 `1234` 登入. 94 | 95 | ### 运行可执行文件 96 | 97 | - **下载**: 下载最新的可执行文件 [Release Page](https://github.com/ihexxa/quickshare/releases). 98 | - **解压**: 解压并执行 `DEFAULTADMIN=qs DEFAULTADMINPWD=1234 ./quickshare`. (你可能需要更新它的执行权限, 比如运行 `chmod u+x quickshare`) 99 | - **访问**: 最后, 打开 `http://127.0.0.1:8686`, 并且使用用户名 `qs` 和 密码 `1234` 登入. 100 | 101 | ### 常见问题 102 | 103 | Coming soon. 104 | -------------------------------------------------------------------------------- /docs/doc.md: -------------------------------------------------------------------------------- 1 | ### File Management 2 | #### Resume Uploading 3 | By clicking the upload button and re-upload the stopped file, the client will resume the uploading. 4 | 5 | #### Move Files or Folders 6 | You can move files or folders by following these steps: 7 | Choose files or folders by ticking them (in the right) 8 | Go to the target folder 9 | Click the “Paste” button at the top of the pane. 10 | 11 | #### Share Directories 12 | You can share a folder and its files by following these steps: 13 | Go to “Files” tab, 14 | Go to the folder you want to share 15 | Click the “Share Folder” button 16 | 17 | #### Cancel Sharings 18 | There are 2 ways to cancel one sharing: 19 | In the “Files” tab, go to the folder and click the “Stop Sharing” button 20 | In the “Sharings” tab, find the target directory and click the “Cancel” button 21 | 22 | #### Manage Files and Folders outside the Docker Container 23 | If the Quickshare is started inside a docker, all files and folders are also persisted inside the docker. Then it is difficult to manage files and folders through the OS. 24 | 25 | Here is a solution: 26 | ##### About Permissions 27 | In the Quickshare docker image, a user `quickshare` (uid=8686) and group `quickshare` (gid=8686) are predefined. Normally in Linux, you can not manage files outside the docker, because your uid is not 8686 and you are not a member of `quickshare` group. By creating a `quickshare` group and adding yourself into it, you are able to manage files: 28 | ``` 29 | groupadd -g 8686 quickshare 30 | usermod -aG quickshare $USER 31 | ``` 32 | 33 | 34 | ##### Use [Bind Mounts](https://docs.docker.com/storage/bind-mounts/) 35 | You can mount a non-empty directory with uid=8686 and gid=8686 in running the docker: 36 | ``` 37 | docker run \ 38 | --name quickshare \ 39 | -d -p 8686:8686 \ 40 | -u 8686:8686 \ 41 | -v `pwd`/non-empty-directory:/quickshare/root \ 42 | -e DEFAULTADMIN=qs \ 43 | -e DEFAULTADMINPWD=1234 \ 44 | hexxa/quickshare 45 | ``` 46 | Then you can find files and folders created by the Quickshare under `non-empty-directory`. 47 | 48 | You can also start a container with a [volume](https://docs.docker.com/storage/volumes/), however it is not easy to manage from the OS in this way. 49 | 50 | ### User Management 51 | #### Add Predefined Users 52 | Predefined users can be added by the config file in the `users.predefinedUsers` array, for example, prepare a partial configuration file `predefined_users.yaml`: 53 | ``` 54 | users: 55 | predefinedUsers:◊ 56 | - name: "user1" 57 | pwd: "Quicksh@re" 58 | role: "user" 59 | - name: "user2" 60 | pwd: "Quicksh@re" 61 | role: "user" 62 | ``` 63 | In the yaml, 2 users are predefined: `user1` and `user2` who are identified by password `Quicksh@re`. 64 | Start the Quickshare by adding this configuration: 65 | ``` 66 | ./quickshare -c predefined_users.yaml 67 | ``` 68 | Then you can see these users in the Settings > Management > Users. 69 | 70 | ### System Management 71 | #### Customized Config 72 | You are able to overwrite default configuration by providing your own configuration. 73 | For example, if you want to turn off the captcha, you can set `captchaEnabled` as `false` in your configuration or create a new configuration `disable_captcha.yaml`: 74 | ``` 75 | users: 76 | captchaEnabled: false 77 | ``` 78 | Then start the Quickshare by appending this configuration: 79 | ``` 80 | ./quickshare -c disable_captcha.yaml 81 | ``` 82 | 83 | #### Background Customization 84 | You can customize the background by following these steps: 85 | Upload the wallpaper to some directory 86 | Share this directory 87 | Copy the link of the wallpaper 88 | Go to `Settings > Preference` and set the Background URL in the Background Pane. 89 | 90 | ### MISC 91 | -------------------------------------------------------------------------------- /docs/imgs/desktop_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/desktop_2.png -------------------------------------------------------------------------------- /docs/imgs/desktop_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/desktop_3.png -------------------------------------------------------------------------------- /docs/imgs/mobile_2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/mobile_2.jpeg -------------------------------------------------------------------------------- /docs/imgs/v0.11.0/newlook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.11.0/newlook.png -------------------------------------------------------------------------------- /docs/imgs/v0.11.0/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.11.0/screenshot.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/files_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/files_panel.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/files_panel_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/files_panel_3.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/files_panel_op.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/files_panel_op.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/management_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/management_1.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/management_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/management_2.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/panels.jpg -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/settings_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/settings_1.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/sharing_dir_qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/sharing_dir_qr_code.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/sharing_panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/sharing_panel.png -------------------------------------------------------------------------------- /docs/imgs/v0.4.20/site_addr_qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.4.20/site_addr_qr_code.png -------------------------------------------------------------------------------- /docs/imgs/v0.5.2/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.5.2/login.png -------------------------------------------------------------------------------- /docs/imgs/v0.5.4/screens.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.5.4/screens.jpg -------------------------------------------------------------------------------- /docs/imgs/v0.5.4/screens_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.5.4/screens_2.jpg -------------------------------------------------------------------------------- /docs/imgs/v0.9.1/quickshare_1920.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/docs/imgs/v0.9.1/quickshare_1920.gif -------------------------------------------------------------------------------- /docs/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Please answer following questions before creating the PR: 2 | 3 | * What is this PR about? 4 | * Is there any test which covers the change? 5 | -------------------------------------------------------------------------------- /docs/screenshots.md: -------------------------------------------------------------------------------- 1 | # v0.5.4 2 | ## Screenshots 3 | ![screenshots 1](./imgs/v0.5.4/screens_2.jpg) 4 | ![Screenshots 2](./imgs/v0.5.4/screens.jpg) 5 | 6 | # v0.5.1 7 | ## Login 8 | ![login](./imgs/v0.5.2/login.png) 9 | 10 | # v0.4.20 11 | 12 | ## Site Address QR code 13 | 14 | ![site_addr_qr_code](./imgs/v0.4.20/site_addr_qr_code.png) 15 | 16 | ## Files Panel 17 | 18 | ![files_panel](./imgs/v0.4.20/files_panel.png) 19 | ![files_panel_op](./imgs/v0.4.20/files_panel_op.png) 20 | ![files_panel_3](./imgs/v0.4.20/files_panel_3.png) 21 | 22 | ## All Panels 23 | 24 | ![panels](./imgs/v0.4.20/panels.jpg) 25 | 26 | ## Settings 27 | 28 | ![settings_1](./imgs/v0.4.20/settings_1.png) 29 | 30 | ## Management 31 | 32 | ![management_1](./imgs/v0.4.20/management_1.png) 33 | ![management_2](./imgs/v0.4.20/management_2.png) 34 | 35 | ## Sharing Panel 36 | 37 | ![sharing_panel](./imgs/v0.4.20/sharing_panel.png) 38 | ![sharing_dir_qr_code](./imgs/v0.4.20/sharing_dir_qr_code.png) 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ihexxa/quickshare 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.8 6 | 7 | require ( 8 | github.com/boltdb/bolt v1.3.1 9 | github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f 10 | github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e 11 | github.com/gin-gonic/gin v1.9.1 12 | github.com/ihexxa/fsearch v0.1.2 13 | github.com/ihexxa/gocfg v0.0.1 14 | github.com/ihexxa/multipart v0.0.0-20210916083128-8584a3f00d1d 15 | github.com/ihexxa/randstr v0.3.0 16 | github.com/jessevdk/go-flags v1.4.0 17 | github.com/mattn/go-sqlite3 v1.14.15 18 | github.com/natefinch/lumberjack v2.0.0+incompatible 19 | github.com/parnurzeal/gorequest v0.2.16 20 | github.com/robbert229/jwt v2.0.0+incompatible 21 | github.com/robfig/cron/v3 v3.0.1 22 | go.uber.org/zap v1.16.0 23 | golang.org/x/crypto v0.36.0 24 | modernc.org/sqlite v1.20.4 25 | ) 26 | 27 | require ( 28 | github.com/bytedance/sonic v1.9.1 // indirect 29 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 30 | github.com/dustin/go-humanize v1.0.0 // indirect 31 | github.com/elazarl/goproxy v0.0.0-20201021153353-00ad82a08272 // indirect 32 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 33 | github.com/gin-contrib/sse v0.1.0 // indirect 34 | github.com/go-playground/locales v0.14.1 // indirect 35 | github.com/go-playground/universal-translator v0.18.1 // indirect 36 | github.com/go-playground/validator/v10 v10.14.0 // indirect 37 | github.com/goccy/go-json v0.10.2 // indirect 38 | github.com/google/uuid v1.3.0 // indirect 39 | github.com/ihexxa/q-radix/v3 v3.0.5 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 42 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 43 | github.com/leodido/go-urn v1.2.4 // indirect 44 | github.com/mattn/go-isatty v0.0.19 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 48 | github.com/pkg/errors v0.9.1 // indirect 49 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect 50 | github.com/smartystreets/goconvey v1.6.4 // indirect 51 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 52 | github.com/ugorji/go/codec v1.2.11 // indirect 53 | go.uber.org/atomic v1.6.0 // indirect 54 | go.uber.org/multierr v1.5.0 // indirect 55 | golang.org/x/arch v0.3.0 // indirect 56 | golang.org/x/mod v0.17.0 // indirect 57 | golang.org/x/net v0.38.0 // indirect 58 | golang.org/x/sync v0.12.0 // indirect 59 | golang.org/x/sys v0.31.0 // indirect 60 | golang.org/x/text v0.23.0 // indirect 61 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 62 | google.golang.org/protobuf v1.33.0 // indirect 63 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 64 | gopkg.in/yaml.v2 v2.4.0 // indirect 65 | gopkg.in/yaml.v3 v3.0.1 // indirect 66 | lukechampine.com/uint128 v1.2.0 // indirect 67 | modernc.org/cc/v3 v3.40.0 // indirect 68 | modernc.org/ccgo/v3 v3.16.13 // indirect 69 | modernc.org/libc v1.22.2 // indirect 70 | modernc.org/mathutil v1.5.0 // indirect 71 | modernc.org/memory v1.4.0 // indirect 72 | modernc.org/opt v0.1.3 // indirect 73 | modernc.org/strutil v1.1.3 // indirect 74 | modernc.org/token v1.0.1 // indirect 75 | moul.io/http2curl v1.0.0 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "src/client/web" 5 | ], 6 | "scripts": { 7 | "build:fe": "yarn && bash scripts/copy_js.sh && webpack --config src/client/web/webpack.app.prod.js", 8 | "client:dev": "yarn && bash scripts/copy_js_dev.sh && webpack --config src/client/web/webpack.app.dev.js --watch", 9 | "start": "yarn build:fe && go run cmd/start/main.go -c `pwd`/configs/dev.yml", 10 | "server:dev": "go run cmd/start/main.go -c `pwd`/configs/dev.yml", 11 | "build": "yarn build:fe && bash scripts/build_be.sh", 12 | "build:docker": "docker build . -f Dockerfile_default -t hexxa/quickshare:latest", 13 | "build:docker:heroku": "docker build . -t hexxa/quickshare:latest" 14 | }, 15 | "dependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /scripts/build_be.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o nounset errexit 3 | 4 | QSROOT=$(pwd) 5 | export QSROOT 6 | rm -r dist && mkdir dist 7 | 8 | # set this for builders behind GFW... 9 | go env -w GOPROXY=https://goproxy.cn,direct 10 | go install github.com/mitchellh/gox@v1.0.1 11 | PATH=$PATH:$HOME/go/bin 12 | cd cmd/start 13 | gox \ 14 | -osarch="windows/386 windows/amd64 darwin/amd64 darwin/arm64 linux/386 linux/amd64 linux/arm linux/arm64" \ 15 | -output "$QSROOT/dist/quickshare_{{.OS}}_{{.Arch}}/quickshare" 16 | 17 | distributions=('quickshare_windows_386' 'quickshare_windows_amd64' 'quickshare_darwin_amd64' 'quickshare_darwin_arm64' 'quickshare_linux_386' 'quickshare_linux_amd64' 'quickshare_linux_arm' 'quickshare_linux_arm64') 18 | 19 | cd "$QSROOT" 20 | for dist in "${distributions[@]}"; do 21 | cp "$QSROOT"/configs/lan.yml "$QSROOT"/dist/"$dist" 22 | zip -r -q "$QSROOT"/dist/"$dist".zip ./dist/"$dist"/* 23 | rm -r "$QSROOT"/dist/"$dist" 24 | done 25 | 26 | echo "Done" 27 | -------------------------------------------------------------------------------- /scripts/build_exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o nounset errexit 3 | 4 | QSROOT=$(pwd) 5 | export QSROOT 6 | rm -r dist && mkdir dist 7 | 8 | # set this for builders behind GFW... 9 | go env -w GOPROXY=https://goproxy.cn,direct 10 | go install github.com/mitchellh/gox@v1.0.1 11 | PATH=$PATH:$HOME/go/bin 12 | cd cmd/start 13 | gox \ 14 | -osarch="linux/amd64" \ 15 | -output "$QSROOT/dist/quickshare/start" 16 | 17 | echo "Done" 18 | -------------------------------------------------------------------------------- /scripts/copy_js.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o nounset errexit 3 | 4 | ROOT=$(pwd) 5 | export ROOT 6 | 7 | mkdir -p "$ROOT/static/public/js" 8 | cp "$ROOT/node_modules/immutable/dist/immutable.min.js" "$ROOT/static/public/js/" 9 | cp "$ROOT/node_modules/react-dom/index.js" "$ROOT/static/public/js/" 10 | cp "$ROOT/node_modules/react/index.js" "$ROOT/static/public/js/" 11 | -------------------------------------------------------------------------------- /scripts/copy_js_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o nounset errexit 3 | 4 | export ROOT=`pwd` 5 | cp $ROOT/node_modules/immutable/dist/immutable.min.js $ROOT/static/public/js/ 6 | cp $ROOT/node_modules/react-dom/index.js $ROOT/static/public/js/react-dom.development.js 7 | cp $ROOT/node_modules/react/jsx-dev-runtime.js $ROOT/static/public/js/react.development.js 8 | -------------------------------------------------------------------------------- /src/client/settings.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/ihexxa/quickshare/src/handlers/settings" 9 | "github.com/parnurzeal/gorequest" 10 | ) 11 | 12 | type SettingsClient struct { 13 | addr string 14 | token *http.Cookie 15 | r *gorequest.SuperAgent 16 | } 17 | 18 | func NewSettingsClient(addr string, token *http.Cookie) *SettingsClient { 19 | gr := gorequest.New() 20 | return &SettingsClient{ 21 | addr: addr, 22 | token: token, 23 | r: gr, 24 | } 25 | } 26 | 27 | func (cl *SettingsClient) url(urlpath string) string { 28 | return fmt.Sprintf("%s%s", cl.addr, urlpath) 29 | } 30 | 31 | func (cl *SettingsClient) Health() (*http.Response, string, []error) { 32 | return cl.r.Options(cl.url("/v2/public/settings/health")). 33 | End() 34 | } 35 | 36 | func (cl *SettingsClient) GetClientCfg() (*http.Response, *settings.ClientCfgMsg, []error) { 37 | resp, body, errs := cl.r.Get(cl.url("/v2/public/settings/client")). 38 | AddCookie(cl.token). 39 | End() 40 | 41 | mResp := &settings.ClientCfgMsg{} 42 | err := json.Unmarshal([]byte(body), mResp) 43 | if err != nil { 44 | errs = append(errs, err) 45 | return nil, nil, errs 46 | } 47 | return resp, mResp, nil 48 | } 49 | 50 | func (cl *SettingsClient) SetClientCfg(cfgMsg *settings.ClientCfgMsg) (*http.Response, string, []error) { 51 | return cl.r.Patch(cl.url("/v2/admin/client")). 52 | AddCookie(cl.token). 53 | Send(cfgMsg). 54 | End() 55 | } 56 | 57 | func (cl *SettingsClient) ReportErrors(reports *settings.ClientErrorReports) (*http.Response, string, []error) { 58 | return cl.r.Post(cl.url("/v2/my/errors")). 59 | AddCookie(cl.token). 60 | Send(reports). 61 | End() 62 | } 63 | 64 | func (cl *SettingsClient) WorkerQueueLen() (*http.Response, *settings.WorkerQueueLenResp, []error) { 65 | resp, body, errs := cl.r.Get(cl.url("/v2/admin/workers/queue-len")). 66 | AddCookie(cl.token). 67 | End() 68 | 69 | mResp := &settings.WorkerQueueLenResp{} 70 | err := json.Unmarshal([]byte(body), mResp) 71 | if err != nil { 72 | errs = append(errs, err) 73 | return nil, nil, errs 74 | } 75 | return resp, mResp, nil 76 | } 77 | -------------------------------------------------------------------------------- /src/client/utils.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "net/http" 4 | 5 | func GetCookie(cookies []*http.Cookie, name string) *http.Cookie { 6 | for _, c := range cookies { 7 | if c.Name == name { 8 | return c 9 | } 10 | } 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /src/client/web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react"], 3 | "env": { 4 | "test": { 5 | "plugins": ["@babel/plugin-transform-runtime"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/client/web/.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/yarn-error.log 3 | **/bundle.js -------------------------------------------------------------------------------- /src/client/web/build/template/index.template.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Quickshare 6 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 98 | 99 | 100 | 101 |

102 | 103 | 104 | -------------------------------------------------------------------------------- /src/client/web/build/template/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Quickshare 6 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 93 | 94 | 98 | 99 | 100 | 101 |
102 | 103 | 104 | -------------------------------------------------------------------------------- /src/client/web/jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(15000); 2 | -------------------------------------------------------------------------------- /src/client/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.2.0", 4 | "description": "web client for quickshare", 5 | "main": "", 6 | "scripts": { 7 | "build": "webpack --config webpack.app.prod.js", 8 | "build:watch": "webpack --config webpack.app.prod.js --watch", 9 | "build:dev": "webpack --config webpack.app.dev.js --watch", 10 | "test": "jest test --maxWorkers=2", 11 | "test:watch": "jest test --watch --maxWorkers=2" 12 | }, 13 | "author": "hexxa", 14 | "license": "LGPL-3.0", 15 | "devDependencies": { 16 | "@babel/plugin-transform-runtime": "^7.4.4", 17 | "@babel/preset-env": "^7.4.4", 18 | "@babel/preset-react": "^7.0.0", 19 | "@tailwindcss/postcss": "^4.1.7", 20 | "@types/assert": "^1.4.2", 21 | "@types/deep-diff": "^1.0.0", 22 | "@types/jest": "^27.0.1", 23 | "@types/object-hash": "^2.2.1", 24 | "assert": "^2.0.0", 25 | "babel-loader": "^8.2.2", 26 | "css-loader": "^7.1.2", 27 | "deep-diff": "^1.0.2", 28 | "html-webpack-plugin": "^5.5.0", 29 | "jest": "^27.2.0", 30 | "postcss": "^8.5.3", 31 | "postcss-loader": "^8.1.1", 32 | "style-loader": "^4.0.0", 33 | "tailwindcss": "^4.1.7", 34 | "terser-webpack-plugin": "^5.2.4", 35 | "ts-jest": "^27.0.5", 36 | "ts-loader": "^9.2.8", 37 | "ts-mockito": "^2.6.1", 38 | "ts-node": "^8.2.0", 39 | "typescript": "^4.1.3", 40 | "webpack": "^5.94.0", 41 | "webpack-bundle-analyzer": "^4.4.2", 42 | "webpack-cli": "^4.2.0", 43 | "webpack-merge": "^4.2.1", 44 | "worker-loader": "^3.0.7" 45 | }, 46 | "dependencies": { 47 | "@babel/preset-react": "7.27.1", 48 | "@react-icons/all-files": "^4.1.0", 49 | "@types/axios": "^0.14.0", 50 | "@types/immutable": "^3.8.7", 51 | "@types/lodash": "^4.14.181", 52 | "@types/react": "19.1.5", 53 | "@types/react-copy-to-clipboard": "5.0.7", 54 | "@types/react-dom": "19.1.5", 55 | "@types/react-svg": "^5.0.0", 56 | "@types/throttle-debounce": "^1.1.1", 57 | "axios": "0.30.0", 58 | "css-loader": "^5.0.0", 59 | "filesize": "^6.1.0", 60 | "immutable": "^4.0.0-rc.12", 61 | "lodash": "^4.17.21", 62 | "object-hash": "^2.2.0", 63 | "react": "19.1.0", 64 | "react-copy-to-clipboard": "5.1.0", 65 | "react-dom": "19.1.0", 66 | "react-icons": "5.5.0", 67 | "react-qr-code": "^2.0.3", 68 | "react-svg": "16.3.0", 69 | "throttle-debounce": "^4.0.1", 70 | "webpack-bundle-analyzer": "^4.4.2", 71 | "worker-loader": "^3.0.7" 72 | }, 73 | "jest": { 74 | "preset": "ts-jest", 75 | "testEnvironment": "jsdom", 76 | "testMatch": [ 77 | "**/src/**/__test__/**/*.test.ts", 78 | "**/src/**/__test__/**/*.test.tsx" 79 | ], 80 | "transform": { 81 | "\\.(ts|tsx)$": "ts-jest" 82 | }, 83 | "verbose": true, 84 | "moduleFileExtensions": [ 85 | "ts", 86 | "tsx", 87 | "js" 88 | ], 89 | "setupFilesAfterEnv": [ 90 | "./jest.setup.js" 91 | ] 92 | }, 93 | "autoBump": {} 94 | } 95 | -------------------------------------------------------------------------------- /src/client/web/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | } 5 | } -------------------------------------------------------------------------------- /src/client/web/src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | 4 | import { StateMgr } from "./components/state_mgr"; 5 | import { ErrorLogger } from "./common/log_error"; 6 | import { errCorsScript } from "./common/errors"; 7 | 8 | import './style/tailwind.css'; 9 | 10 | window.onerror = ( 11 | msg: string, 12 | source: string, 13 | lineno: number, 14 | colno: number, 15 | error: Error 16 | ) => { 17 | const lowerMsg = msg.toLowerCase(); 18 | if (lowerMsg.indexOf("script error") > -1) { 19 | ErrorLogger().error("Check Browser Console for Detail"); 20 | } 21 | ErrorLogger().error(`${source}:${lineno}:${colno}: ${error.toString()}`); 22 | }; 23 | 24 | const root = ReactDOM.createRoot(document.getElementById("mount")); 25 | root.render(); 26 | -------------------------------------------------------------------------------- /src/client/web/src/client/settings.ts: -------------------------------------------------------------------------------- 1 | import { List } from "immutable"; 2 | 3 | import { BaseClient, Response, userIDParam, Quota } from "."; 4 | import { ClientConfigMsg, ClientErrorReport } from "./"; 5 | 6 | export class SettingsClient extends BaseClient { 7 | constructor(url: string) { 8 | super(url); 9 | } 10 | 11 | health = (): Promise => { 12 | return this.do({ 13 | method: "get", 14 | url: `${this.url}/v2/public/settings/health`, 15 | }); 16 | }; 17 | 18 | getClientCfg = (): Promise => { 19 | return this.do({ 20 | method: "get", 21 | url: `${this.url}/v2/public/settings/client`, 22 | }); 23 | }; 24 | 25 | setClientCfg = (cfg: ClientConfigMsg): Promise => { 26 | return this.do({ 27 | method: "patch", 28 | url: `${this.url}/v2/admin/client`, 29 | data: cfg, 30 | }); 31 | }; 32 | 33 | reportErrors = (reports: List): Promise => { 34 | return this.do({ 35 | method: "post", 36 | url: `${this.url}/v2/my/errors`, 37 | data: { 38 | reports: reports.toArray(), 39 | }, 40 | }); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/client/web/src/client/settings_mock.ts: -------------------------------------------------------------------------------- 1 | import { Response, ISettingsClient, ClientConfigMsg } from "./"; 2 | import { makePromise } from "../test/helpers"; 3 | 4 | export interface SettingsClientResps { 5 | healthMockResp?: Response; 6 | setClientCfgMockResp?: Response; 7 | getClientCfgMockResp?: Response; 8 | reportErrorsMockResp?: Response; 9 | } 10 | 11 | export const resps = { 12 | healthMockResp: { status: 200, statusText: "", data: {} }, 13 | setClientCfgMockResp: { status: 200, statusText: "", data: {} }, 14 | getClientCfgMockResp: { 15 | status: 200, 16 | statusText: "", 17 | data: { 18 | clientCfg: { 19 | siteName: "", 20 | siteDesc: "", 21 | bg: { 22 | url: "clientCfg_bg_url", 23 | repeat: "clientCfg_bg_repeat", 24 | position: "clientCfg_bg_position", 25 | align: "clientCfg_bg_align", 26 | bgColor: "clientCfg_bg_bg_Color" 27 | }, 28 | allowSetBg: true, 29 | autoTheme: true, 30 | }, 31 | captchaEnabled: true, 32 | }, 33 | }, 34 | reportErrorsMockResp: { 35 | status: 200, 36 | statusText: "", 37 | data: {}, 38 | }, 39 | }; 40 | 41 | export class JestSettingsClient { 42 | url: string = ""; 43 | constructor(url: string) { 44 | this.url = url; 45 | } 46 | 47 | health = jest.fn().mockReturnValue(makePromise(resps.healthMockResp)); 48 | getClientCfg = jest 49 | .fn() 50 | .mockReturnValue(makePromise(resps.getClientCfgMockResp)); 51 | setClientCfg = jest 52 | .fn() 53 | .mockReturnValue(makePromise(resps.setClientCfgMockResp)); 54 | reportErrors = jest 55 | .fn() 56 | .mockReturnValue(makePromise(resps.reportErrorsMockResp)); 57 | } 58 | 59 | export const NewMockSettingsClient = (url: string): ISettingsClient => { 60 | return new JestSettingsClient(url); 61 | }; 62 | -------------------------------------------------------------------------------- /src/client/web/src/client/users.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient, Response, userIDParam, Quota, Preferences } from "./"; 2 | 3 | export class UsersClient extends BaseClient { 4 | constructor(url: string) { 5 | super(url); 6 | } 7 | 8 | login = ( 9 | user: string, 10 | pwd: string, 11 | captchaId: string, 12 | captchaInput: string 13 | ): Promise => { 14 | return this.do({ 15 | method: "post", 16 | url: `${this.url}/v2/public/login`, 17 | data: { 18 | user, 19 | pwd, 20 | captchaId, 21 | captchaInput, 22 | }, 23 | }); 24 | }; 25 | 26 | logout = (): Promise => { 27 | return this.do({ 28 | method: "post", 29 | url: `${this.url}/v2/my/logout`, 30 | }); 31 | }; 32 | 33 | isAuthed = (): Promise => { 34 | return this.do({ 35 | method: "get", 36 | url: `${this.url}/v2/my/isauthed`, 37 | }); 38 | }; 39 | 40 | setPwd = (oldPwd: string, newPwd: string): Promise => { 41 | return this.do({ 42 | method: "patch", 43 | url: `${this.url}/v2/my/pwd`, 44 | data: { 45 | oldPwd, 46 | newPwd, 47 | }, 48 | }); 49 | }; 50 | 51 | setUser = (id: string, role: string, quota: Quota): Promise => { 52 | return this.do({ 53 | method: "patch", 54 | url: `${this.url}/v2/admin/users/`, 55 | data: { 56 | id, 57 | role, 58 | quota, 59 | }, 60 | }); 61 | }; 62 | 63 | forceSetPwd = (userID: string, newPwd: string): Promise => { 64 | return this.do({ 65 | method: "patch", 66 | url: `${this.url}/v2/admin/users/pwd/force-set`, 67 | data: { 68 | id: userID, 69 | newPwd, 70 | }, 71 | }); 72 | }; 73 | 74 | // token cookie is set by browser 75 | addUser = (name: string, pwd: string, role: string): Promise => { 76 | return this.do({ 77 | method: "post", 78 | url: `${this.url}/v2/admin/users/`, 79 | data: { 80 | name, 81 | pwd, 82 | role, 83 | }, 84 | }); 85 | }; 86 | 87 | delUser = (userID: string): Promise => { 88 | return this.do({ 89 | method: "delete", 90 | url: `${this.url}/v2/admin/users/`, 91 | params: { 92 | [userIDParam]: userID, 93 | }, 94 | }); 95 | }; 96 | 97 | listUsers = (): Promise => { 98 | return this.do({ 99 | method: "get", 100 | url: `${this.url}/v2/admin/users/list`, 101 | params: {}, 102 | }); 103 | }; 104 | 105 | // deprecated 106 | addRole = (role: string): Promise => { 107 | return this.do({ 108 | method: "post", 109 | url: `${this.url}/v1/roles/`, 110 | data: { role }, 111 | }); 112 | }; 113 | 114 | // deprecated 115 | delRole = (role: string): Promise => { 116 | return this.do({ 117 | method: "delete", 118 | url: `${this.url}/v1/roles/`, 119 | data: { role }, 120 | }); 121 | }; 122 | 123 | listRoles = (): Promise => { 124 | return this.do({ 125 | method: "get", 126 | url: `${this.url}/v2/admin/roles/list`, 127 | params: {}, 128 | }); 129 | }; 130 | 131 | self = (): Promise => { 132 | return this.do({ 133 | method: "get", 134 | url: `${this.url}/v2/my/self`, 135 | params: {}, 136 | }); 137 | }; 138 | 139 | getCaptchaID = (): Promise => { 140 | return this.do({ 141 | method: "get", 142 | url: `${this.url}/v2/public/captchas`, 143 | params: {}, 144 | }); 145 | }; 146 | 147 | setPreferences = (prefers: Preferences): Promise => { 148 | return this.do({ 149 | method: "patch", 150 | url: `${this.url}/v2/my/preferences`, 151 | data: { 152 | preferences: prefers, 153 | }, 154 | }); 155 | }; 156 | } 157 | -------------------------------------------------------------------------------- /src/client/web/src/common/controls.ts: -------------------------------------------------------------------------------- 1 | export const settingsTabsCtrl = "settingsTabs"; 2 | export const settingsDialogCtrl = "settingsDialog"; 3 | export const sharingCtrl = "sharingCtrl"; 4 | export const filesViewCtrl = "filesView"; 5 | export const dropAreaCtrl = "dropArea"; 6 | export const ctrlHidden = "hidden"; 7 | export const ctrlOn = "on"; 8 | export const ctrlOff = "off"; 9 | export const loadingCtrl = "loading"; -------------------------------------------------------------------------------- /src/client/web/src/common/cron.ts: -------------------------------------------------------------------------------- 1 | import { Map } from "immutable"; 2 | 3 | export interface CronTask { 4 | func: (arg?: any, ...optionalArgs: any[]) => void; 5 | delay: number; 6 | args?: any[]; 7 | handler?: number; 8 | } 9 | 10 | export class Cron { 11 | private tasks: Map; 12 | constructor() { 13 | this.tasks = Map(); 14 | } 15 | 16 | setInterval = (name: string, task: CronTask) => { 17 | if (this.tasks.has(name)) { 18 | this.clearInterval(name); 19 | } 20 | 21 | const handler = window.setInterval(task.func, task.delay, ...task.args); 22 | task.handler = handler; 23 | this.tasks = this.tasks.set(name, task); 24 | }; 25 | 26 | clearInterval = (name: string) => { 27 | const preTask = this.tasks.get(name); 28 | window.clearInterval(preTask.handler); 29 | this.tasks = this.tasks.delete(name); 30 | }; 31 | 32 | getTasks = (): Map => { 33 | return this.tasks; 34 | }; 35 | } 36 | 37 | const cronJobs = new Cron(); 38 | export const CronJobs = (): Cron => { 39 | return cronJobs; 40 | }; 41 | -------------------------------------------------------------------------------- /src/client/web/src/common/env.ts: -------------------------------------------------------------------------------- 1 | export class WebEnv { 2 | constructor() {} 3 | 4 | alertMsg = (msg: string) => { 5 | if (alert != null) { 6 | alert(msg); 7 | } else { 8 | console.log(msg); 9 | } 10 | }; 11 | 12 | confirmMsg = (msg: string): boolean => { 13 | if (confirm != null) { 14 | return confirm(msg); 15 | } else { 16 | console.warn(`${msg}: return yes (confirm is not implemented)`); 17 | return true; 18 | } 19 | }; 20 | } 21 | 22 | export interface IEnv { 23 | alertMsg: (msg: string) => void; 24 | confirmMsg: (msg: string) => boolean; 25 | } 26 | 27 | let env = new WebEnv(); 28 | export const Env = (): IEnv => env; 29 | export const SetEnv = (expectedEnv: IEnv) => { 30 | env = expectedEnv; 31 | }; 32 | -------------------------------------------------------------------------------- /src/client/web/src/common/errors.ts: -------------------------------------------------------------------------------- 1 | export const errUpdater = "err.updater"; 2 | export const errUploadMgr = "err.uploadMgr"; 3 | export const errServer = "err.server"; 4 | export const errCorsScript = "err.script.cors"; 5 | export const errUnknown = "err.unknown"; 6 | -------------------------------------------------------------------------------- /src/client/web/src/common/hotkeys.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Map } from "immutable"; 3 | 4 | export interface Hotkey { 5 | key: string; 6 | ctrl?: boolean; 7 | shift?: boolean; 8 | alt?: boolean; 9 | meta?: boolean; 10 | repeat?: boolean; 11 | ev?: KeyboardEvent; 12 | } 13 | 14 | export type HotkeyCb = (hotKey?: Hotkey) => void; 15 | 16 | export class HotkeyHandler { 17 | private keyMap: Map; 18 | 19 | constructor() { 20 | this.keyMap = Map(); 21 | } 22 | 23 | getSign = (hk: Hotkey): string => { 24 | let sign = hk.key; 25 | sign = hk.ctrl != null && hk.ctrl ? `${sign}+ctrl` : sign; 26 | sign = hk.shift != null && hk.shift ? `${sign}+shift` : sign; 27 | sign = hk.alt != null && hk.alt ? `${sign}+alt` : sign; 28 | sign = hk.meta != null && hk.meta ? `${sign}+meta` : sign; 29 | sign = hk.repeat != null && hk.repeat ? `${sign}+repeat` : sign; 30 | 31 | return sign; 32 | }; 33 | 34 | add = (hk: Hotkey, handler: HotkeyCb) => { 35 | const sign = this.getSign(hk); 36 | this.keyMap = this.keyMap.set(sign, handler); 37 | }; 38 | 39 | handle = (ev: KeyboardEvent) => { 40 | ev.preventDefault(); 41 | ev.stopPropagation(); 42 | 43 | const hotKey = { 44 | key: ev.key, 45 | ctrl: ev.ctrlKey, 46 | shift: ev.shiftKey, 47 | alt: ev.altKey, 48 | meta: ev.metaKey, 49 | repeat: ev.repeat, 50 | }; 51 | const sign = this.getSign(hotKey); 52 | 53 | if (this.keyMap.has(sign)) { 54 | const handler = this.keyMap.get(sign); 55 | handler(hotKey); 56 | } 57 | }; 58 | 59 | printMap = () => { 60 | console.log(this.keyMap.toMap()); 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/client/web/src/common/localstorage.ts: -------------------------------------------------------------------------------- 1 | export interface ILocalStorage { 2 | get: (key: string) => string; 3 | set: (key: string, val: string) => void; 4 | } 5 | 6 | export const errNoLocalStorage = "local storage is not supported"; 7 | 8 | class LocalStorage { 9 | constructor() {} 10 | 11 | get(key: string): string { 12 | if (window != null && window.localStorage != null) { 13 | const val = window.localStorage.getItem(key); 14 | return val && val != "undefined" && val != "null" ? val : ""; 15 | } 16 | 17 | return ""; 18 | } 19 | 20 | set(key: string, val: string) { 21 | if (window != null && window.localStorage != null) { 22 | window.localStorage.setItem(key, val); 23 | } else { 24 | console.error(errNoLocalStorage); 25 | } 26 | } 27 | } 28 | 29 | var localStorage: LocalStorage; 30 | export const Storage = () => { 31 | if (localStorage == null) { 32 | localStorage = new LocalStorage(); 33 | } 34 | return localStorage; 35 | }; 36 | -------------------------------------------------------------------------------- /src/client/web/src/common/log_error.ts: -------------------------------------------------------------------------------- 1 | import { Map, List } from "immutable"; 2 | import { sha1 } from "object-hash"; 3 | 4 | import { ILocalStorage, Storage } from "./localstorage"; 5 | import { ISettingsClient } from "../client"; 6 | import { SettingsClient } from "../client/settings"; 7 | import { ICoreState } from "../components/core_state"; 8 | import { updater } from "../components/state_updater"; 9 | import { ClientErrorReport } from "../client"; 10 | 11 | const errorVer = "0.0.1"; 12 | const cookieKeyClErrs = "qs_cli_errs"; 13 | 14 | export interface ClientErrorV001 { 15 | error: string; 16 | timestamp: string; 17 | state: ICoreState; 18 | } 19 | 20 | export interface IErrorLogger { 21 | setClient: (client: ISettingsClient) => void; 22 | setStorage: (storage: ILocalStorage) => void; 23 | error: (msg: string) => null | Error; 24 | report: () => Promise; 25 | readErrs: () => Map; 26 | truncate: () => void; 27 | } 28 | 29 | export class SimpleErrorLogger { 30 | private client: ISettingsClient; 31 | private storage: ILocalStorage = Storage(); 32 | 33 | constructor(client: ISettingsClient) { 34 | this.client = client; 35 | } 36 | 37 | setClient(client: ISettingsClient) { 38 | this.client = client; 39 | } 40 | 41 | setStorage(storage: ILocalStorage) { 42 | this.storage = storage; 43 | } 44 | 45 | private getErrorSign = (errMsg: string): string => { 46 | return `e:${sha1(errMsg)}`; 47 | }; 48 | 49 | readErrs = (): Map => { 50 | try { 51 | const errsStr = this.storage.get(cookieKeyClErrs); 52 | if (errsStr === "") { 53 | return Map(); 54 | } 55 | 56 | const errsObj = JSON.parse(errsStr); 57 | return Map(errsObj); 58 | } catch (e: any) { 59 | this.truncate(); // reset 60 | } 61 | return Map(); 62 | }; 63 | 64 | private writeErrs = (errs: Map) => { 65 | const errsObj = errs.toObject(); 66 | const errsStr = JSON.stringify(errsObj); 67 | this.storage.set(cookieKeyClErrs, errsStr); 68 | }; 69 | 70 | error = (msg: string): null | Error => { 71 | try { 72 | const sign = this.getErrorSign(msg); 73 | const clientErr: ClientErrorV001 = { 74 | error: msg, 75 | timestamp: `${Date.now()}`, 76 | state: updater().props, 77 | }; 78 | let errs = this.readErrs(); 79 | if (!errs.has(sign)) { 80 | errs = errs.set(sign, clientErr); 81 | this.writeErrs(errs); 82 | } 83 | } catch (err: any) { 84 | return Error(`failed to save err log: ${err}`); 85 | } 86 | 87 | return null; 88 | }; 89 | 90 | report = async (): Promise => { 91 | try { 92 | const errs = this.readErrs(); 93 | let reports = List(); 94 | for (let sign of errs.keySeq().toArray()) { 95 | const errObj = errs.get(sign); 96 | reports = reports.push({ 97 | report: JSON.stringify(errObj), 98 | version: errorVer, 99 | }); 100 | } 101 | 102 | const resp = await this.client.reportErrors(reports); 103 | if (resp.status !== 200) { 104 | return Error(`failed to report error: ${resp.data}`); 105 | } else { 106 | this.truncate(); 107 | } 108 | } catch (e: any) { 109 | return Error(e); 110 | } 111 | 112 | return null; 113 | }; 114 | 115 | truncate = () => { 116 | this.writeErrs(Map()); 117 | }; 118 | } 119 | 120 | const errorLogger = new SimpleErrorLogger(new SettingsClient("")); 121 | export const ErrorLogger = (): IErrorLogger => { 122 | return errorLogger; 123 | }; 124 | -------------------------------------------------------------------------------- /src/client/web/src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { List, Map } from "immutable"; 2 | 3 | 4 | export interface Row { 5 | val: Object; // original object value 6 | sortVals: List; // sortable values in order 7 | } 8 | 9 | export function getItemPath(dirPath: string, itemName: string): string { 10 | return dirPath.endsWith("/") 11 | ? `${dirPath}${itemName}` 12 | : `${dirPath}/${itemName}`; 13 | } 14 | 15 | export function getErrMsg( 16 | msgPkg: Map, 17 | msg: string, 18 | status: string 19 | ): string { 20 | return `${msgPkg.get(msg)}: ${msgPkg.get(status)}`; 21 | } 22 | 23 | export function sortRows( 24 | rows: List, 25 | key: number, 26 | order: boolean 27 | ): List { 28 | return rows.sort((row1: Row, row2: Row) => { 29 | const val1 = row1.sortVals.get(key); 30 | const val2 = row2.sortVals.get(key); 31 | 32 | if (val1 == null || val2 == null) { 33 | // elements without the sort key will be moved to the last 34 | if (val1 == null && val2 != null) { 35 | return 1; 36 | } else if (val1 != null && val2 == null) { 37 | return -1; 38 | } 39 | return 0; 40 | } else if (val1 < val2) { 41 | return order ? -1 : 1; 42 | } else if (val1 === val2) { 43 | return 0; 44 | } 45 | return order ? 1 : -1; 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/client/web/src/components/__test__/pane_settings.test.tsx: -------------------------------------------------------------------------------- 1 | import { mock, instance } from "ts-mockito"; 2 | 3 | import { PaneSettings } from "../pane_settings"; 4 | import { initUploadMgr } from "../../worker/upload_mgr"; 5 | import { ICoreState, newState } from "../core_state"; 6 | import { updater } from "../state_updater"; 7 | import { MockWorker } from "../../worker/interface"; 8 | import { 9 | NewMockUsersClient, 10 | resps as usersResps, 11 | } from "../../client/users_mock"; 12 | import { 13 | NewMockFilesClient, 14 | resps as filesResps, 15 | } from "../../client/files_mock"; 16 | import { NewMockSettingsClient } from "../../client/settings_mock"; 17 | import { MockWebEnv } from "../../test/helpers"; 18 | import { SetEnv } from "../../common/env"; 19 | 20 | describe("PaneSettings", () => { 21 | const initPaneSettings = (): any => { 22 | const mockWorkerClass = mock(MockWorker); 23 | const mockWorker = instance(mockWorkerClass); 24 | initUploadMgr(mockWorker); 25 | 26 | const coreState = newState(); 27 | const usersCl = NewMockUsersClient(""); 28 | const filesCl = NewMockFilesClient(""); 29 | const settingsCl = NewMockSettingsClient(""); 30 | 31 | updater().init(coreState); 32 | updater().setClients(usersCl, filesCl, settingsCl); 33 | 34 | const paneSettings = new PaneSettings({ 35 | msg: coreState.msg, 36 | login: coreState.login, 37 | ui: coreState.ui, 38 | update: (updater: (prevState: ICoreState) => ICoreState) => {}, 39 | }); 40 | 41 | return { 42 | paneSettings, 43 | usersCl, 44 | filesCl, 45 | settingsCl, 46 | }; 47 | }; 48 | 49 | test("Preferences settings alerts", async () => { 50 | const { paneSettings, usersCl, filesCl, settingCl } = initPaneSettings(); 51 | const env = new MockWebEnv(); 52 | SetEnv(env); 53 | 54 | await paneSettings.setLan("en_US"); 55 | expect(env.alertMsg.mock.calls.length).toBe(1); 56 | 57 | await paneSettings.setTheme("light"); 58 | expect(env.alertMsg.mock.calls.length).toBe(2); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/client/web/src/components/__test__/panel_files.test.tsx: -------------------------------------------------------------------------------- 1 | import { List } from "immutable"; 2 | import * as immutable from "immutable"; 3 | 4 | import { initMockWorker } from "../../test/helpers"; 5 | import { FilesPanel } from "../panel_files"; 6 | import { ICoreState, newState } from "../core_state"; 7 | import { updater } from "../state_updater"; 8 | import { 9 | NewMockUsersClient, 10 | resps as usersResps, 11 | } from "../../client/users_mock"; 12 | import { 13 | NewMockFilesClient, 14 | resps as filesResps, 15 | } from "../../client/files_mock"; 16 | import { NewMockSettingsClient } from "../../client/settings_mock"; 17 | import { makePromise } from "../../test/helpers"; 18 | 19 | describe("FilesPanel", () => { 20 | const initFilesPanel = (): any => { 21 | initMockWorker(); 22 | 23 | const coreState = newState(); 24 | const usersCl = NewMockUsersClient(""); 25 | const filesCl = NewMockFilesClient(""); 26 | const settingsCl = NewMockSettingsClient(""); 27 | 28 | updater().init(coreState); 29 | updater().setClients(usersCl, filesCl, settingsCl); 30 | 31 | const filesPanel = new FilesPanel({ 32 | filesInfo: coreState.filesInfo, 33 | msg: coreState.msg, 34 | login: coreState.login, 35 | ui: coreState.ui, 36 | enabled: true, 37 | update: (updater: (prevState: ICoreState) => ICoreState) => { }, 38 | }); 39 | 40 | return { 41 | filesPanel, 42 | usersCl, 43 | filesCl, 44 | }; 45 | }; 46 | 47 | test("chdir", async () => { 48 | const { filesPanel, usersCl, filesCl } = initFilesPanel(); 49 | 50 | const newCwd = List(["newPos", "subFolder"]); 51 | 52 | await filesPanel.chdir(newCwd); 53 | 54 | expect(updater().props.filesInfo.dirPath).toEqual(newCwd); 55 | expect(updater().props.filesInfo.isSharing).toEqual(true); 56 | expect(updater().props.filesInfo.items).toEqual( 57 | List([ 58 | { 59 | name: "mock_dir", 60 | size: 0, 61 | modTime: "0", 62 | isDir: true, 63 | sha1: "", 64 | }, 65 | { 66 | name: "mock_file", 67 | size: 5, 68 | modTime: "0", 69 | isDir: false, 70 | sha1: "mock_file_sha1", 71 | }, 72 | ]) 73 | ); 74 | }); 75 | 76 | test("addSharing", async () => { 77 | const { filesPanel, usersCl, filesCl } = initFilesPanel(); 78 | 79 | const newSharingPath = List(["newPos", "subFolder"]); 80 | const sharingDir = newSharingPath.join("/"); 81 | const newSharings = immutable.Map({ 82 | [sharingDir]: "f123456", 83 | }); 84 | const newSharingsResp = new Map(); 85 | newSharingsResp.set(sharingDir, "f123456"); 86 | 87 | filesCl.listSharingIDs = jest.fn().mockReturnValueOnce( 88 | makePromise({ 89 | status: 200, 90 | statusText: "", 91 | data: { 92 | IDs: newSharingsResp, 93 | }, 94 | }) 95 | ); 96 | 97 | await filesPanel.addSharing(newSharingPath); 98 | 99 | expect(updater().props.filesInfo.isSharing).toEqual(true); 100 | expect(updater().props.sharingsInfo.sharings).toEqual(newSharings); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/client/web/src/components/__test__/panel_sharings.test.tsx: -------------------------------------------------------------------------------- 1 | import { mock, instance, verify, when, anything } from "ts-mockito"; 2 | import { Map } from "immutable"; 3 | 4 | import { SharingsPanel } from "../panel_sharings"; 5 | import { initUploadMgr } from "../../worker/upload_mgr"; 6 | import { ICoreState, newState } from "../core_state"; 7 | import { updater } from "../state_updater"; 8 | import { MockWorker } from "../../worker/interface"; 9 | import { NewMockUsersClient } from "../../client/users_mock"; 10 | import { NewMockFilesClient } from "../../client/files_mock"; 11 | import { NewMockSettingsClient } from "../../client/settings_mock"; 12 | import { makePromise } from "../../test/helpers"; 13 | 14 | describe("SharingsPanel", () => { 15 | const initSharingsPanel = (): any => { 16 | const mockWorkerClass = mock(MockWorker); 17 | const mockWorker = instance(mockWorkerClass); 18 | initUploadMgr(mockWorker); 19 | 20 | const coreState = newState(); 21 | const usersCl = NewMockUsersClient(""); 22 | const filesCl = NewMockFilesClient(""); 23 | const settingsCl = NewMockSettingsClient(""); 24 | 25 | updater().init(coreState); 26 | updater().setClients(usersCl, filesCl, settingsCl); 27 | 28 | const sharingsPanel = new SharingsPanel({ 29 | sharingsInfo: coreState.sharingsInfo, 30 | msg: coreState.msg, 31 | login: coreState.login, 32 | ui: coreState.ui, 33 | update: (updater: (prevState: ICoreState) => ICoreState) => {}, 34 | }); 35 | 36 | return { 37 | sharingsPanel, 38 | usersCl, 39 | filesCl, 40 | }; 41 | }; 42 | 43 | test("delete sharing", async () => { 44 | const { sharingsPanel, usersCl, filesCl } = initSharingsPanel(); 45 | 46 | const newSharings = Map({ 47 | mock_sharingfolder1: "f123456", 48 | mock_sharingfolder2: "f123456", 49 | }); 50 | 51 | filesCl.listSharingIDs = jest.fn().mockReturnValueOnce( 52 | makePromise({ 53 | status: 200, 54 | statusText: "", 55 | data: { 56 | // it seems immutable map will be converted into built-in map automatically 57 | IDs: newSharings, 58 | }, 59 | }) 60 | ); 61 | 62 | await sharingsPanel.deleteSharing(); 63 | 64 | // TODO: check delSharing's input 65 | expect(updater().props.filesInfo.isSharing).toEqual(false); 66 | expect(updater().props.sharingsInfo.sharings).toEqual(newSharings); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/client/web/src/components/__test__/panel_uploadings.test.tsx: -------------------------------------------------------------------------------- 1 | import { mock, instance } from "ts-mockito"; 2 | 3 | import { UploadingsPanel } from "../panel_uploadings"; 4 | import { initUploadMgr } from "../../worker/upload_mgr"; 5 | import { ICoreState, newState } from "../core_state"; 6 | import { updater } from "../state_updater"; 7 | import { MockWorker } from "../../worker/interface"; 8 | import { NewMockUsersClient, resps as usersResps } from "../../client/users_mock"; 9 | import { NewMockFilesClient, resps as filesResps } from "../../client/files_mock"; 10 | import { NewMockSettingsClient } from "../../client/settings_mock"; 11 | 12 | describe("UploadingsPanel", () => { 13 | const initUploadingsPanel = (): any => { 14 | const mockWorkerClass = mock(MockWorker); 15 | const mockWorker = instance(mockWorkerClass); 16 | initUploadMgr(mockWorker); 17 | 18 | const coreState = newState(); 19 | const usersCl = NewMockUsersClient(""); 20 | const filesCl = NewMockFilesClient(""); 21 | const settingsCl = NewMockSettingsClient(""); 22 | 23 | updater().init(coreState); 24 | updater().setClients(usersCl, filesCl, settingsCl); 25 | 26 | const uploadingsPanel = new UploadingsPanel({ 27 | uploadingsInfo: coreState.uploadingsInfo, 28 | msg: coreState.msg, 29 | login: coreState.login, 30 | ui: coreState.ui, 31 | update: (updater: (prevState: ICoreState) => ICoreState) => {}, 32 | }); 33 | 34 | return { 35 | uploadingsPanel, 36 | usersCl, 37 | filesCl, 38 | }; 39 | }; 40 | 41 | test("todo", async () => {}); 42 | }); 43 | -------------------------------------------------------------------------------- /src/client/web/src/components/api.ts: -------------------------------------------------------------------------------- 1 | import { ICoreState } from "./core_state"; 2 | import { updater, Updater } from "./state_updater"; 3 | import { UploadEntry } from "../worker/interface"; 4 | import { MetadataResp } from "../client"; 5 | 6 | export class QuickshareAPI { 7 | private updater: Updater; 8 | constructor() { 9 | this.updater = updater(); 10 | } 11 | initAll = async (params: URLSearchParams): Promise => { 12 | return this.updater.initAll(params); 13 | }; 14 | 15 | addUploadArray = (fileArray: Array): string => { 16 | return this.updater.addUploadArray(fileArray); 17 | }; 18 | 19 | deleteUpload = async (filePath: string): Promise => { 20 | return await this.updater.deleteUpload(filePath); 21 | }; 22 | 23 | listUploadArray = async (): Promise> => { 24 | return await this.updater.listUploadArray(); 25 | }; 26 | 27 | stopUploading = (filePath: string): string => { 28 | return this.updater.stopUploading(filePath); 29 | }; 30 | 31 | self = async (): Promise => { 32 | return await this.updater.self(); 33 | }; 34 | 35 | getProps = (): ICoreState => { 36 | return this.updater.props; 37 | }; 38 | 39 | deleteInArray = async (itemsToDel: Array): Promise => { 40 | return await this.updater.deleteInArray(itemsToDel); 41 | }; 42 | } 43 | 44 | const api = new QuickshareAPI(); 45 | export const API = (): QuickshareAPI => { 46 | return api; 47 | }; 48 | -------------------------------------------------------------------------------- /src/client/web/src/components/control/btn_list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | import { Flexbox } from "../layout/flexbox"; 5 | import { getIconWithProps } from "../visual/icons"; 6 | 7 | export type BtnListCallBack = () => void; 8 | export interface Props { 9 | titleIcon?: string; 10 | btnNames: List; 11 | btnCallbacks: List; 12 | } 13 | 14 | export const BtnList = (props: Props) => { 15 | const titleIcon = 16 | props.titleIcon != null ? ( 17 | getIconWithProps(props.titleIcon, { 18 | size: "1.8rem", 19 | className: "major-font mr-4", 20 | }) 21 | ) : ( 22 | 23 | ); 24 | 25 | const btns = props.btnNames.map((btnName: string, i: number) => { 26 | const cb = props.btnCallbacks.get(i); 27 | const isLast = i === props.btnNames.size - 1; 28 | return ( 29 | 36 | ); 37 | }); 38 | 39 | return ( 40 |
41 | {btns}])} 43 | childrenStyles={List([{ flex: "0 0 auto" }, { flex: "0 0 auto" }])} 44 | /> 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/client/web/src/components/control/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List, Map } from "immutable"; 3 | 4 | import { updater } from "../state_updater"; 5 | import { ICoreState, MsgProps, UIProps } from "../core_state"; 6 | import { Env } from "../../common/env"; 7 | import { IconProps, getIcon } from "../visual/icons"; 8 | import { colorClass } from "../visual/colors"; 9 | import { Flexbox } from "../layout/flexbox"; 10 | 11 | const defaultIconProps: IconProps = { 12 | name: "RiFolder2Fill", 13 | size: "1.6rem", 14 | color: `${colorClass("cyan1")}`, 15 | }; 16 | 17 | export interface Props { 18 | targetControl: string; 19 | tabIcons: Map; // option name -> icon name 20 | titleIcon?: string; 21 | ui: UIProps; 22 | msg: MsgProps; 23 | update?: (updater: (prevState: ICoreState) => ICoreState) => void; 24 | } 25 | 26 | export interface State {} 27 | export class Tabs extends React.Component { 28 | constructor(p: Props) { 29 | super(p); 30 | } 31 | 32 | setTab = (targetControl: string, targetOption: string) => { 33 | if (!updater().setControlOption(targetControl, targetOption)) { 34 | Env().alertMsg(this.props.msg.pkg.get("op.fail")); 35 | } 36 | this.props.update(updater().updateUI); 37 | }; 38 | 39 | render() { 40 | const displaying = this.props.ui.control.controls.get( 41 | this.props.targetControl 42 | ); 43 | 44 | const titleIcon = 45 | this.props.titleIcon != null 46 | ? getIcon(this.props.titleIcon, "2rem", "normal") 47 | : null; 48 | const options = this.props.ui.control.options.get(this.props.targetControl); 49 | const tabs = options.map((option: string) => { 50 | const iconProps = this.props.tabIcons.has(option) 51 | ? this.props.tabIcons.get(option) 52 | : defaultIconProps; 53 | 54 | const iconColor = displaying === option ? iconProps.color : "normal"; 55 | const icon = getIcon(iconProps.name, iconProps.size, iconColor); 56 | const fontWeight = 57 | displaying === option ? `font-bold` : ""; 58 | 59 | return ( 60 | 75 | ); 76 | }); 77 | 78 | return ( 79 | 82 | {tabs} 83 |
84 | , 85 | 86 | {getIcon("FaGithub", "2rem", "normal")} 87 | , 88 | ])} 89 | childrenStyles={List([ 90 | { flex: "0 0 auto" }, 91 | { justifyContent: "flex-end" }, 92 | ])} 93 | /> 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/client/web/src/components/dialog_settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Map } from "immutable"; 3 | 4 | import { ICoreState, MsgProps, UIProps } from "./core_state"; 5 | import { IconProps, iconSize } from "./visual/icons"; 6 | 7 | import { PaneSettings } from "./pane_settings"; 8 | import { AdminPane, AdminProps } from "./pane_admin"; 9 | 10 | import { Tabs } from "./control/tabs"; 11 | import { Container } from "./layout/container"; 12 | import { LoginProps } from "./pane_login"; 13 | import { roleAdmin } from "../client"; 14 | import { settingsTabsCtrl } from "../common/controls"; 15 | 16 | export interface Props { 17 | admin: AdminProps; 18 | login: LoginProps; 19 | msg: MsgProps; 20 | ui: UIProps; 21 | update?: (updater: (prevState: ICoreState) => ICoreState) => void; 22 | } 23 | 24 | export interface State {} 25 | export class SettingsDialog extends React.Component { 26 | constructor(p: Props) { 27 | super(p); 28 | } 29 | 30 | render() { 31 | const displaying = this.props.ui.control.controls.get(settingsTabsCtrl); 32 | const showSettings = displaying === "preferencePane" ? "" : "hidden"; 33 | const showManagement = 34 | this.props.login.userRole === roleAdmin && displaying === "managementPane" 35 | ? "" 36 | : "hidden"; 37 | 38 | return ( 39 |
40 | 41 | ({ 44 | preferencePane: { 45 | name: "RiSettings3Fill", 46 | size: iconSize("s"), 47 | color: "focus", 48 | }, 49 | managementPane: { 50 | name: "RiWindowFill", 51 | size: iconSize("s"), 52 | color: "focus", 53 | }, 54 | })} 55 | ui={this.props.ui} 56 | msg={this.props.msg} 57 | update={this.props.update} 58 | /> 59 | 60 | 61 |
62 | 68 |
69 | 70 |
71 | 77 |
78 |
79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/client/web/src/components/layout/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface Props { 4 | name: string; 5 | value: string; 6 | } 7 | 8 | export const Card = (props: Props) => { 9 | return ( 10 |
11 |
{props.value}
12 |
{props.name}
13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/client/web/src/components/layout/columns.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | export interface Props { 5 | rows: List>; 6 | widths: List; 7 | childrenClassNames?: List; 8 | style?: React.CSSProperties; 9 | className?: string; 10 | colKey?: string; 11 | } 12 | 13 | export const Columns = (props: Props) => { 14 | const children = props.rows.map( 15 | (row: List, i: number): React.ReactNode => { 16 | const cells = row.map((cell: React.ReactNode, j: number) => { 17 | const width = props.widths.get(j, Math.trunc(100 / row.size)); 18 | const className = props.childrenClassNames.get(j, ""); 19 | 20 | return ( 21 |
26 | {cell} 27 |
28 | ); 29 | }); 30 | 31 | return ( 32 |
33 | {cells} 34 |
35 |
36 | ); 37 | } 38 | ); 39 | 40 | return ( 41 |
42 | {children} 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/client/web/src/components/layout/container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface Props { 4 | children: React.ReactNode | undefined; 5 | } 6 | 7 | export const Container = (props: Props) => { 8 | return ( 9 |
10 |
{props.children}
11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/client/web/src/components/layout/flexbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | export interface Props { 5 | children: List; 6 | childrenStyles?: List; 7 | style?: React.CSSProperties; 8 | className?: string; 9 | } 10 | 11 | const containerStyle = { 12 | display: "flex", 13 | flex: "row nowrap", 14 | alignItems: "center", 15 | justifyContent: "flex-start", 16 | }; 17 | 18 | const childrenStyle = { 19 | flex: "50%", 20 | display: "flex", 21 | alignItems: "flex-start", 22 | justifyContent: "flex-start", 23 | }; 24 | 25 | export const Flexbox = (props: Props) => { 26 | const childrenCount = props.children.size; 27 | const children = props.children.map( 28 | (child: React.ReactNode, i: number): React.ReactNode => { 29 | return ( 30 |
40 | {child} 41 |
42 | ); 43 | } 44 | ); 45 | 46 | return ( 47 |
51 | {children} 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/client/web/src/components/layout/flowgrid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | export interface Props { 5 | grids: List; 6 | style?: React.CSSProperties; 7 | className?: string; 8 | } 9 | 10 | export const Flowgrid = (props: Props) => { 11 | const children = props.grids.map( 12 | (child: React.ReactNode, i: number): React.ReactNode => { 13 | return ( 14 | 15 | {child} 16 | 17 | ); 18 | } 19 | ); 20 | 21 | return ( 22 |
23 | {children} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/client/web/src/components/layout/rows.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | export interface Row { 5 | elem: React.ReactNode; // element to display 6 | val: Object; // original object value 7 | sortVals: List; // sortable values in order 8 | } 9 | 10 | export interface Props { 11 | rows: List; 12 | id?: string; 13 | style?: React.CSSProperties; 14 | className?: string; 15 | } 16 | 17 | export interface State {} 18 | 19 | export class Rows extends React.Component { 20 | constructor(p: Props) { 21 | super(p); 22 | } 23 | 24 | render() { 25 | const bodyRows = this.props.rows.map( 26 | (row: React.ReactNode, i: number): React.ReactNode => { 27 | return
{row}
; 28 | } 29 | ); 30 | 31 | return ( 32 |
37 | {bodyRows} 38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/client/web/src/components/layout/segments.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | export interface Props { 5 | id?: string; 6 | children: List; 7 | ratios: List; 8 | dir: boolean; // true=left, false=right 9 | className?: string; 10 | } 11 | 12 | export const Segments = (props: Props) => { 13 | let sum = 0; 14 | props.ratios.forEach((ratio) => { 15 | sum += Math.trunc(ratio); 16 | }); 17 | if (sum > 100) { 18 | throw `segments: ratio sum(${sum}) > 100`; 19 | } else if (props.children.size !== props.ratios.size) { 20 | throw `segments: children size(${props.children.size}) != ratio size(${props.ratios.size})`; 21 | } 22 | 23 | const children = props.children.map( 24 | (child: React.ReactNode, i: number): React.ReactNode => { 25 | const width = `${props.ratios.get(i, 0)}%`; 26 | return ( 27 |
31 | {child} 32 |
33 | ); 34 | } 35 | ); 36 | 37 | return ( 38 |
39 |
{children}
40 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/client/web/src/components/state_mgr.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { initUploadMgr } from "../worker/upload_mgr"; 4 | import BgWorker from "../worker/upload.bg.worker"; 5 | import { FgWorker } from "../worker/upload.fg.worker"; 6 | 7 | import { Env } from "../common/env"; 8 | import { getErrMsg } from "../common/utils"; 9 | import { updater } from "./state_updater"; 10 | import { ICoreState, newState } from "./core_state"; 11 | import { RootFrame } from "./root_frame"; 12 | import { FilesClient } from "../client/files"; 13 | import { UsersClient } from "../client/users"; 14 | import { SettingsClient } from "../client/settings"; 15 | import { IUsersClient, IFilesClient, ISettingsClient } from "../client"; 16 | import { loadingCtrl, ctrlOn, ctrlOff } from "../common/controls"; 17 | import { CronJobs } from "../common/cron"; 18 | 19 | export interface Props {} 20 | export interface State extends ICoreState {} 21 | 22 | export class StateMgr extends React.Component { 23 | private usersClient: IUsersClient = new UsersClient(""); 24 | private filesClient: IFilesClient = new FilesClient(""); 25 | private settingsClient: ISettingsClient = new SettingsClient(""); 26 | 27 | constructor(p: Props) { 28 | super(p); 29 | const worker = window.Worker == null ? new FgWorker() : new BgWorker(); 30 | initUploadMgr(worker); 31 | this.state = newState(); 32 | 33 | const query = new URLSearchParams(document.location.search.substring(1)); 34 | this.initUpdater(this.state, query); // don't await 35 | } 36 | 37 | componentDidMount(): void { 38 | CronJobs().setInterval("refreshState", { 39 | func: this.update, 40 | args: [updater().updateAll], 41 | delay: 1000, 42 | }); 43 | } 44 | 45 | componentWillUnmount() { 46 | CronJobs().clearInterval("refreshState"); 47 | } 48 | 49 | setUsersClient = (client: IUsersClient) => { 50 | this.usersClient = client; 51 | }; 52 | 53 | setFilesClient = (client: IFilesClient) => { 54 | this.filesClient = client; 55 | }; 56 | 57 | setSettingsClient = (client: ISettingsClient) => { 58 | this.settingsClient = client; 59 | }; 60 | 61 | initUpdater = async ( 62 | state: ICoreState, 63 | query: URLSearchParams 64 | ): Promise => { 65 | updater().init(state); 66 | updater().setControlOption(loadingCtrl, ctrlOn); 67 | this.update(updater().updateUI); 68 | 69 | if ( 70 | this.usersClient == null || 71 | this.filesClient == null || 72 | this.settingsClient == null 73 | ) { 74 | console.error("updater's clients are not inited"); 75 | return; 76 | } 77 | updater().setClients( 78 | this.usersClient, 79 | this.filesClient, 80 | this.settingsClient 81 | ); 82 | 83 | const status = await updater().initAll(query); 84 | if (status !== "") { 85 | Env().alertMsg(getErrMsg(state.msg.pkg, "op.fail", status)); 86 | } 87 | updater().setControlOption(loadingCtrl, ctrlOff); 88 | this.update(updater().updateAll); 89 | }; 90 | 91 | update = (update: (prevState: ICoreState) => ICoreState): void => { 92 | this.setState(update(this.state)); 93 | }; 94 | 95 | render() { 96 | return ( 97 | 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/client/web/src/components/topbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | import { Env } from "../common/env"; 5 | import { ICoreState, MsgProps, UIProps } from "./core_state"; 6 | import { LoginProps } from "./pane_login"; 7 | import { updater } from "./state_updater"; 8 | import { Flexbox } from "./layout/flexbox"; 9 | import { ctrlOn, ctrlHidden, settingsDialogCtrl } from "../common/controls"; 10 | import { Container } from "./layout/container"; 11 | // import { QRCodeIcon } from "./visual/qrcode"; 12 | 13 | export interface State {} 14 | export interface Props { 15 | login: LoginProps; 16 | msg: MsgProps; 17 | ui: UIProps; 18 | update?: (updater: (prevState: ICoreState) => ICoreState) => void; 19 | } 20 | 21 | export class TopBar extends React.Component { 22 | constructor(p: Props) { 23 | super(p); 24 | } 25 | 26 | openSettings = () => { 27 | updater().setControlOption(settingsDialogCtrl, ctrlOn); 28 | this.props.update(updater().updateUI); 29 | }; 30 | 31 | logout = async (): Promise => { 32 | if (!Env().confirmMsg(this.props.msg.pkg.get("logout.confirm"))) { 33 | return; 34 | } 35 | 36 | const status = await updater().logout(); 37 | if (status !== "") { 38 | Env().alertMsg(this.props.msg.pkg.get("login.logout.fail")); 39 | return; 40 | } 41 | 42 | const params = new URLSearchParams(document.location.search.substring(1)); 43 | const initStatus = await updater().initAll(params); 44 | if (initStatus !== "") { 45 | Env().alertMsg(this.props.msg.pkg.get("op.fail")); 46 | return; 47 | } 48 | this.props.update(updater().updateAll); 49 | }; 50 | 51 | render() { 52 | const loginPanelClass = this.props.login.authed ? "" : "hidden"; 53 | const settingsPanelClass = 54 | this.props.ui.control.controls.get(settingsDialogCtrl) === ctrlHidden 55 | ? "hidden" 56 | : ""; 57 | 58 | return ( 59 |
60 | 69 | {this.props.ui.clientCfg.siteName} 70 | , 71 | 72 | {this.props.ui.clientCfg.siteDesc} 73 | , 74 | // , 80 | 81 | 87 | {this.props.msg.pkg.get("settings")} 88 | , 89 | 90 | , 96 | ])} 97 | childrenStyles={List([ 98 | { flex: "0 0 auto" }, 99 | { flex: "0 0 auto" }, 100 | ])} 101 | />, 102 | ])} 103 | childrenStyles={List([ 104 | { flex: "0 0 auto" }, 105 | { flex: "0 0 auto" }, 106 | // { flex: "0 0 auto" }, 107 | { justifyContent: "flex-end", alignItems: "center" }, 108 | ])} 109 | /> 110 |
111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/client/web/src/components/visual/banner_notfound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | import { RiFileList2Fill } from "@react-icons/all-files/ri/RiFileList2Fill"; 5 | 6 | import { Flexbox } from "../layout/flexbox"; 7 | 8 | export interface Props { 9 | title: string; 10 | } 11 | 12 | export const NotFoundBanner = (props: Props) => { 13 | return ( 14 | , 17 | 18 |

{props.title}

19 |
, 20 | ])} 21 | childrenStyles={List([ 22 | { flex: "auto", justifyContent: "flex-end" }, 23 | { flex: "auto" }, 24 | ])} 25 | className="margin-t-l margin-b-l" 26 | /> 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/client/web/src/components/visual/colors.ts: -------------------------------------------------------------------------------- 1 | import { Set } from "immutable"; 2 | 3 | export const colors = Set([ 4 | "blue0", 5 | "blue1", 6 | "blue2", 7 | "cyan0", 8 | "cyan1", 9 | "purple0", 10 | "purple1", 11 | "red0", 12 | "red1", 13 | "yellow0", 14 | "yellow1", 15 | "yellow2", 16 | "yellow3", 17 | "yellow3", 18 | "green0", 19 | "green1", 20 | "green2", 21 | "white", 22 | "white0", 23 | "white1", 24 | "grey0", 25 | "grey1", 26 | "grey2", 27 | "grey3", 28 | "black", 29 | "black0", 30 | "black1", 31 | "dark", 32 | "light", 33 | "major", 34 | "minor", 35 | "focus", 36 | "normal", 37 | "focus", 38 | "error", 39 | ]); 40 | 41 | export function colorClass(name: string): string { 42 | if (!colors.has(name)) { 43 | console.error(`color ${name} not found`); 44 | return colors.get("black"); 45 | } 46 | return colors.get(name); 47 | } 48 | -------------------------------------------------------------------------------- /src/client/web/src/components/visual/loading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { RiLoader5Fill } from "@react-icons/all-files/ri/RiLoader5Fill"; 4 | 5 | export interface Props {} 6 | 7 | export interface State {} 8 | 9 | export const LoadingIcon = (props: Props) => { 10 | return ( 11 |
12 | 17 | 22 | 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/client/web/src/components/visual/qrcode.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import QRCode from "react-qr-code"; 3 | 4 | import { RiQrCodeFill } from "@react-icons/all-files/ri/RiQrCodeFill"; 5 | 6 | export interface Props { 7 | value: string; 8 | size: number; 9 | pos: boolean; // true=left, right=false 10 | className?: string; 11 | } 12 | 13 | export interface State { 14 | show: boolean; 15 | } 16 | 17 | export class QRCodeIcon extends React.Component { 18 | constructor(p: Props) { 19 | super(p); 20 | this.state = { 21 | show: false, 22 | }; 23 | } 24 | 25 | toggle = () => { 26 | this.setState({ show: !this.state.show }); 27 | }; 28 | 29 | show = () => { 30 | this.setState({ show: true }); 31 | }; 32 | hide = () => { 33 | this.setState({ show: false }); 34 | }; 35 | 36 | render() { 37 | const widthInRem = `${Math.floor(this.props.size / 10)}rem`; 38 | const posStyle = this.props.pos 39 | ? { left: `0` } 40 | : { left: `-${widthInRem}` }; 41 | const qrcode = this.state.show ? ( 42 |
43 |
44 | 45 |
46 |
47 | ) : null; 48 | 49 | return ( 50 |
51 | 58 |
{qrcode}
59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/client/web/src/components/visual/title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List } from "immutable"; 3 | 4 | import { Flexbox } from "../layout/flexbox"; 5 | 6 | export interface Props { 7 | iconName: string; 8 | iconColor: string; 9 | title: string; 10 | } 11 | 12 | export const Title = (props: Props) => { 13 | return ( 14 | 17 | {/*
18 | {getIconWithProps(props.iconName, { 19 | size: iconSize("l"), 20 | className: `margin-r-m ${colorClass(props.iconColor)}-font`, 21 | })} 22 |
*/} 23 |
{props.title}
24 | , 25 | , 26 | ])} 27 | /> 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/client/web/src/i18n/msger.ts: -------------------------------------------------------------------------------- 1 | import { Map, Set } from "immutable"; 2 | 3 | import { msgs as enMsgs } from "./en_US"; 4 | import { msgs as cnMsgs } from "./zh_CN"; 5 | 6 | export class Msger { 7 | private msgs: Map; 8 | constructor(msgs: Map) { 9 | this.msgs = msgs; 10 | } 11 | m(key: string): string { 12 | return this.msgs.get(key, ""); 13 | } 14 | } 15 | 16 | export class MsgPackage { 17 | static get(key: string): Map { 18 | switch (key) { 19 | case "en_US": 20 | return Map(enMsgs); 21 | case "zh_CN": 22 | return Map(cnMsgs); 23 | default: 24 | return Map(enMsgs); 25 | } 26 | } 27 | } 28 | 29 | export function isValidLanPack(lanPackObject: any): boolean { 30 | const topLevelkeys = Set(Object.keys(lanPackObject)); 31 | if (!topLevelkeys.has("lan") && !topLevelkeys.has("pkg")) { 32 | return false; 33 | } 34 | 35 | const gotKeys = Set(Object.keys(lanPackObject.pkg)); 36 | let missingKeys = Set(); 37 | lanPackKeys.forEach((key: string) => { 38 | if (!gotKeys.has(key)) { 39 | missingKeys = missingKeys.add(key); 40 | } 41 | }); 42 | 43 | // TODO: provide better error report? 44 | if (missingKeys.size > 0) { 45 | console.error(missingKeys); 46 | } 47 | return missingKeys.size > 0; 48 | } 49 | 50 | export const lanPackKeys = Set([ 51 | "stateMgr.cap.fail", 52 | "browser.upload.del.fail", 53 | "browser.folder.add.fail", 54 | "browser.del.fail", 55 | "browser.move.fail", 56 | "browser.share.add.fail", 57 | "browser.share.del.fail", 58 | "browser.share.del", 59 | "browser.share.add", 60 | "browser.share.title", 61 | "browser.share.desc", 62 | "browser.upload.title", 63 | "browser.upload.desc", 64 | "browser.folder.name", 65 | "browser.folder.add", 66 | "browser.upload", 67 | "browser.delete", 68 | "browser.paste", 69 | "browser.select", 70 | "browser.deselect", 71 | "browser.selectAll", 72 | "browser.stop", 73 | "browser.location", 74 | "browser.item.title", 75 | "browser.used", 76 | "panes.close", 77 | "login.logout.fail", 78 | "login.username", 79 | "login.captcha", 80 | "login.pwd", 81 | "login.login", 82 | "login.logout", 83 | "settings.pwd.notSame", 84 | "settings.pwd.empty", 85 | "settings.pwd.notChanged", 86 | "update", 87 | "settings.pwd.old", 88 | "settings.pwd.new1", 89 | "settings.pwd.new2", 90 | "settings", 91 | "settings.chooseLan", 92 | "settings.pwd.update", 93 | "admin", 94 | "update.ok", 95 | "update.fail", 96 | "delete.fail", 97 | "delete.ok", 98 | "delete", 99 | "spaceLimit", 100 | "uploadLimit", 101 | "downloadLimit", 102 | "add.fail", 103 | "add.ok", 104 | "role.delete.warning", 105 | "user.id", 106 | "user.add", 107 | "user.name", 108 | "user.role", 109 | "user.password", 110 | "add", 111 | "admin.users", 112 | "role.add", 113 | "role.name", 114 | "admin.roles", 115 | "zhCN", 116 | "enUS", 117 | "move.fail", 118 | "share.404.title", 119 | "share.404.desc", 120 | "upload.404.title", 121 | "upload.404.desc", 122 | "detail", 123 | "refresh", 124 | "refresh-hint", 125 | "pane.login", 126 | "pane.admin", 127 | "pane.settings", 128 | "logout.confirm", 129 | "unauthed", 130 | "err.tooManyUploads", 131 | "login.role", 132 | "user.profile", 133 | "user.downLimit", 134 | "user.upLimit", 135 | "user.spaceLimit", 136 | "cfg.siteName", 137 | "cfg.siteDesc", 138 | "cfg.bg", 139 | "cfg.bg.url", 140 | "cfg.bg.repeat", 141 | "cfg.bg.pos", 142 | "cfg.bg.align", 143 | "reset", 144 | "bg.url.alert", 145 | "bg.pos.alert", 146 | "bg.align.alert", 147 | "prefer.theme", 148 | "prefer.theme.url", 149 | "settings.customLan", 150 | "settings.lanPackURL", 151 | ]); 152 | -------------------------------------------------------------------------------- /src/client/web/src/style/tailwind.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /src/client/web/src/test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { mock, instance } from "ts-mockito"; 2 | import { List } from "immutable"; 3 | 4 | import { MockWorker } from "../worker/interface"; 5 | import { initUploadMgr } from "../worker/upload_mgr"; 6 | import { Response } from "../client"; 7 | import { ICoreState, initState } from "../components/core_state"; 8 | 9 | export const makePromise = (ret: any): Promise => { 10 | return new Promise((resolve) => { 11 | resolve(ret); 12 | }); 13 | }; 14 | 15 | export const makeNumberResponse = (status: number): Promise => { 16 | return makePromise({ 17 | status: status, 18 | statusText: "", 19 | data: {}, 20 | }); 21 | }; 22 | 23 | export const mockUpdate = ( 24 | apply: (prevState: ICoreState) => ICoreState 25 | ): void => { 26 | apply(initState()); 27 | }; 28 | 29 | export const addMockUpdate = (subState: any) => { 30 | subState.update = mockUpdate; 31 | }; 32 | 33 | export function mockRandFile(filePath: string): File { 34 | const values = new Array(Math.floor(7 * Math.random())); 35 | const content = [values.join("")]; 36 | return new File(content, filePath); 37 | } 38 | 39 | export function mockFileList(filePaths: Array): List { 40 | const files = filePaths.map((filePath) => { 41 | return mockRandFile(filePath); 42 | }); 43 | return List(files); 44 | } 45 | 46 | export function initMockWorker() { 47 | const mockWorkerClass = mock(MockWorker); 48 | const mockWorker = instance(mockWorkerClass); 49 | initUploadMgr(mockWorker); 50 | } 51 | 52 | export class MockWebEnv { 53 | constructor() {} 54 | 55 | alertMsg = jest.fn(); 56 | confirmMsg = jest.fn(); 57 | } 58 | -------------------------------------------------------------------------------- /src/client/web/src/typings/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "worker-loader!*" { 2 | class UploadWorker extends Worker { 3 | constructor(); 4 | } 5 | 6 | export = UploadWorker; 7 | } 8 | -------------------------------------------------------------------------------- /src/client/web/src/worker/__test__/upload.worker.test.ts: -------------------------------------------------------------------------------- 1 | import { mock, instance, when } from "ts-mockito"; 2 | 3 | import { UploadWorker } from "../upload.baseworker"; 4 | import { FileUploader } from "../uploader"; 5 | import { FileWorkerResp, UploadEntry, syncReqKind, UploadState } from "../interface"; 6 | 7 | describe("upload.worker", () => { 8 | const content = ["123456"]; 9 | const filePath = "mock/file"; 10 | const blob = new Blob(content); 11 | const fileSize = blob.size; 12 | const file = new File(content, filePath); 13 | 14 | const makeEntry = (filePath: string, state: UploadState): UploadEntry => { 15 | return { 16 | file: file, 17 | filePath, 18 | size: fileSize, 19 | uploaded: 0, 20 | state, 21 | err: "", 22 | }; 23 | }; 24 | 25 | xtest("onMsg:syncReqKind: filter list and start uploading correct file", async () => {}); 26 | }); 27 | -------------------------------------------------------------------------------- /src/client/web/src/worker/interface.ts: -------------------------------------------------------------------------------- 1 | export const enum UploadState { 2 | Created, 3 | Ready, 4 | Uploading, 5 | Stopped, 6 | Error, 7 | } 8 | 9 | export interface UploadStatus { 10 | filePath: string; 11 | uploaded: number; 12 | state: UploadState; 13 | err: string; 14 | } 15 | 16 | export interface UploadEntry { 17 | file: File; 18 | filePath: string; 19 | size: number; 20 | uploaded: number; 21 | state: UploadState; 22 | err: string; 23 | } 24 | 25 | export interface IChunkUploader { 26 | create: (filePath: string, file: File) => Promise; 27 | upload: ( 28 | filePath: string, 29 | file: File, 30 | uploaded: number 31 | ) => Promise; 32 | } 33 | 34 | export type eventKind = SyncReqKind | ErrKind | UploadInfoKind | ImIdleKind; 35 | export interface WorkerEvent { 36 | kind: eventKind; 37 | } 38 | 39 | export type SyncReqKind = "worker.req.sync"; 40 | export const syncReqKind: SyncReqKind = "worker.req.sync"; 41 | 42 | export interface SyncReq extends WorkerEvent { 43 | kind: SyncReqKind; 44 | file: File; 45 | filePath: string; 46 | size: number; 47 | uploaded: number; 48 | created: boolean; 49 | } 50 | 51 | export type FileWorkerReq = SyncReq; 52 | 53 | export type ErrKind = "worker.resp.err"; 54 | export const errKind: ErrKind = "worker.resp.err"; 55 | export interface ErrResp extends WorkerEvent { 56 | kind: ErrKind; 57 | filePath: string; 58 | err: string; 59 | } 60 | 61 | // caller should combine uploaded and done to see if the upload is successfully finished 62 | export type UploadInfoKind = "worker.resp.info"; 63 | export const uploadInfoKind: UploadInfoKind = "worker.resp.info"; 64 | export interface UploadInfoResp extends WorkerEvent { 65 | kind: UploadInfoKind; 66 | filePath: string; 67 | uploaded: number; 68 | state: UploadState; 69 | err: string; 70 | } 71 | 72 | export type ImIdleKind = "worker.resp.idle"; 73 | export const imIdleKind: ImIdleKind = "worker.resp.idle"; 74 | export interface ImIdleResp extends WorkerEvent { 75 | kind: ImIdleKind; 76 | } 77 | 78 | export type FileWorkerResp = ErrResp | UploadInfoResp | ImIdleResp; 79 | 80 | export class MockWorker { 81 | constructor() {} 82 | onmessage = (event: MessageEvent): void => {}; 83 | postMessage = (event: FileWorkerReq): void => {}; 84 | } 85 | -------------------------------------------------------------------------------- /src/client/web/src/worker/upload.baseworker.ts: -------------------------------------------------------------------------------- 1 | import { ChunkUploader } from "./chunk_uploader"; 2 | import { 3 | FileWorkerReq, 4 | syncReqKind, 5 | SyncReq, 6 | errKind, 7 | ErrResp, 8 | ImIdleResp, 9 | uploadInfoKind, 10 | imIdleKind, 11 | UploadInfoResp, 12 | FileWorkerResp, 13 | UploadStatus, 14 | UploadState, 15 | IChunkUploader, 16 | } from "./interface"; 17 | 18 | const win: Window = self as any; 19 | 20 | export class UploadWorker { 21 | private uploader: IChunkUploader = new ChunkUploader(); 22 | private cycle: number = 100; 23 | private working: boolean = false; 24 | 25 | sendEvent = (resp: FileWorkerResp): void => { 26 | // TODO: make this abstract 27 | throw new Error("not implemented"); 28 | }; 29 | 30 | constructor() { 31 | win.setInterval(this.checkIdle, this.cycle); 32 | } 33 | 34 | checkIdle = () => { 35 | if (this.working) { 36 | return; 37 | } 38 | 39 | const resp: ImIdleResp = { 40 | kind: imIdleKind, 41 | }; 42 | this.sendEvent(resp); 43 | }; 44 | 45 | setUploader = (uploader: IChunkUploader) => { 46 | this.uploader = uploader; 47 | }; 48 | 49 | handleUploadStatus = (status: UploadStatus) => { 50 | if (status.state !== UploadState.Error) { 51 | const resp: UploadInfoResp = { 52 | kind: uploadInfoKind, 53 | filePath: status.filePath, 54 | uploaded: status.uploaded, 55 | state: status.state, 56 | err: "", 57 | }; 58 | this.sendEvent(resp); 59 | } else { 60 | const resp: ErrResp = { 61 | kind: errKind, 62 | filePath: status.filePath, 63 | err: status.err, 64 | }; 65 | this.sendEvent(resp); 66 | } 67 | }; 68 | 69 | onMsg = async (event: MessageEvent) => { 70 | try { 71 | this.working = true; 72 | const req = event.data as FileWorkerReq; 73 | 74 | switch (req.kind) { 75 | case syncReqKind: 76 | const syncReq = req as SyncReq; 77 | 78 | if (syncReq.created) { 79 | if (syncReq.file.size === 0) { 80 | const resp: UploadInfoResp = { 81 | kind: uploadInfoKind, 82 | filePath: syncReq.filePath, 83 | uploaded: 0, 84 | state: UploadState.Ready, 85 | err: "", 86 | }; 87 | this.sendEvent(resp); 88 | } else { 89 | const status = await this.uploader.upload( 90 | syncReq.filePath, 91 | syncReq.file, 92 | syncReq.uploaded 93 | ); 94 | await this.handleUploadStatus(status); 95 | } 96 | } else { 97 | const status = await this.uploader.create( 98 | syncReq.filePath, 99 | syncReq.file 100 | ); 101 | await this.handleUploadStatus(status); 102 | } 103 | break; 104 | default: 105 | console.error(`unknown worker request(${JSON.stringify(req)})`); 106 | } 107 | } finally { 108 | this.working = false; 109 | } 110 | }; 111 | 112 | onError = (ev: ErrorEvent) => { 113 | const errResp: ErrResp = { 114 | kind: errKind, 115 | filePath: "unknown", 116 | err: ev.error, 117 | }; 118 | this.sendEvent(errResp); 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /src/client/web/src/worker/upload.bg.worker.ts: -------------------------------------------------------------------------------- 1 | import { UploadWorker } from "./upload.baseworker"; 2 | import { FileWorkerResp } from "./interface"; 3 | 4 | const ctx: Worker = self as any; 5 | 6 | class BgWorker extends UploadWorker { 7 | constructor() { 8 | super(); 9 | } 10 | 11 | sendEvent = (resp: FileWorkerResp): void => { 12 | ctx.postMessage(resp); 13 | }; 14 | } 15 | 16 | const worker = new BgWorker(); 17 | ctx.addEventListener("message", worker.onMsg); 18 | ctx.addEventListener("error", worker.onError); 19 | 20 | export default null as any; 21 | -------------------------------------------------------------------------------- /src/client/web/src/worker/upload.fg.worker.ts: -------------------------------------------------------------------------------- 1 | import { UploadWorker } from "./upload.baseworker"; 2 | import { FileWorkerReq, FileWorkerResp } from "./interface"; 3 | 4 | export class FgWorker extends UploadWorker { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | // provide interfaces for non-worker mode 10 | onmessage = (event: MessageEvent): void => {}; 11 | 12 | sendEvent = (resp: FileWorkerResp) => { 13 | this.onmessage( 14 | new MessageEvent("worker", { 15 | data: resp, 16 | }) 17 | ); 18 | }; 19 | 20 | postMessage = (req: FileWorkerReq): void => { 21 | this.onMsg( 22 | new MessageEvent("worker", { 23 | data: req, 24 | }) 25 | ); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/client/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [`${__dirname}/build/template/*.html`], 3 | theme: { 4 | extend: {}, 5 | }, 6 | variants: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /src/client/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es5", 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "allowJs": true, 11 | "lib": ["es5", "dom", "scripthost", "es2015.symbol"] 12 | }, 13 | "include": ["./src/**/*"], 14 | "exclude": ["**/*.test.ts*"] 15 | } 16 | -------------------------------------------------------------------------------- /src/client/web/webpack.app.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | const dev = require("./webpack.dev.js"); 5 | 6 | module.exports = merge(dev, { 7 | plugins: [ 8 | new HtmlWebpackPlugin({ 9 | template: `${__dirname}/build/template/index.template.dev.html`, 10 | hash: true, 11 | filename: `../index.html`, 12 | }), 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /src/client/web/webpack.app.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | const prod = require("./webpack.prod.js"); 5 | 6 | module.exports = merge(prod, { 7 | plugins: [ 8 | new HtmlWebpackPlugin({ 9 | template: `${__dirname}/build/template/index.template.html`, 10 | hash: true, 11 | filename: `../index.html`, 12 | minify: false, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /src/client/web/webpack.common.js: -------------------------------------------------------------------------------- 1 | // const webpack = require("webpack"); 2 | // const CleanWebpackPlugin = require("clean-webpack-plugin"); 3 | // const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 4 | const path = require("path"); 5 | const TerserPlugin = require("terser-webpack-plugin"); 6 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer") 7 | .BundleAnalyzerPlugin; 8 | 9 | 10 | module.exports = { 11 | entry: ["./src/app.tsx", "./src/components/api.ts"], 12 | context: `${__dirname}`, 13 | output: { 14 | globalObject: "this", 15 | path: `${__dirname}/../../../static/public/js`, 16 | chunkFilename: "[name].bundle.js", 17 | filename: "[name].bundle.js", 18 | library: "Q", 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.worker\.ts$/, 24 | use: { 25 | loader: "worker-loader", 26 | options: { 27 | // inline: "fallback", 28 | }, 29 | }, 30 | }, 31 | { 32 | test: /\.ts|tsx$/, 33 | loader: "ts-loader", 34 | include: [path.resolve(__dirname, "src")], 35 | exclude: [/node_modules/, /\.test\.(ts|tsx)$/], 36 | }, 37 | { 38 | test: /\.css$/i, 39 | include: [path.resolve(__dirname, 'src')], 40 | use: ['style-loader', 'css-loader', 'postcss-loader'], 41 | }, 42 | ], 43 | }, 44 | resolve: { 45 | extensions: [".ts", ".tsx", ".js", ".json"], 46 | }, 47 | plugins: [ 48 | // new BundleAnalyzerPlugin() 49 | ], 50 | externals: { 51 | // react: "React", 52 | // "react-dom": "ReactDOM", 53 | immutable: "Immutable", 54 | }, 55 | optimization: { 56 | minimizer: [new TerserPlugin()], 57 | splitChunks: { 58 | chunks: "all", 59 | automaticNameDelimiter: ".", 60 | cacheGroups: { 61 | default: { 62 | name: "main", 63 | filename: "[name].bundle.js", 64 | }, 65 | commons: { 66 | name: "vendors", 67 | test: /[\\/]node_modules[\\/]/, 68 | chunks: "all", 69 | minChunks: 2, 70 | reuseExistingChunk: true, 71 | } 72 | }, 73 | }, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/client/web/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "development", 6 | devtool: "inline-source-map", 7 | // entry: { 8 | // api_test: "./libs/test/api_test" 9 | // }, 10 | watchOptions: { 11 | aggregateTimeout: 1000, 12 | poll: 1000, 13 | ignored: /node_modules/ 14 | }, 15 | plugins: [] 16 | }); 17 | -------------------------------------------------------------------------------- /src/client/web/webpack.prod.js: -------------------------------------------------------------------------------- 1 | // const webpack = require("webpack"); 2 | const merge = require("webpack-merge"); 3 | 4 | const common = require("./webpack.common.js"); 5 | 6 | module.exports = merge(common, { 7 | mode: "production" 8 | }); 9 | -------------------------------------------------------------------------------- /src/cron/wrapper.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import cronv3 "github.com/robfig/cron/v3" 4 | 5 | type ICron interface { 6 | AddFun(spec string, cmd func()) error 7 | Start() 8 | Stop() 9 | } 10 | 11 | type MyCron struct { 12 | *cronv3.Cron 13 | } 14 | 15 | func NewMyCron() *MyCron { 16 | return &MyCron{ 17 | Cron: cronv3.New(), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cryptoutil/cryptoutil_interface.go: -------------------------------------------------------------------------------- 1 | package cryptoutil 2 | 3 | type ITokenEncDec interface { 4 | FromToken(token string, kvs map[string]string) (map[string]string, error) 5 | ToToken(kvs map[string]string) (string, error) 6 | } 7 | -------------------------------------------------------------------------------- /src/cryptoutil/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | 6 | jwtpkg "github.com/robbert229/jwt" 7 | ) 8 | 9 | type JWTEncDec struct { 10 | alg jwtpkg.Algorithm 11 | } 12 | 13 | func NewJWTEncDec(secret string) *JWTEncDec { 14 | return &JWTEncDec{ 15 | alg: jwtpkg.HmacSha256(secret), 16 | } 17 | } 18 | 19 | func (ed *JWTEncDec) FromToken(token string, kvs map[string]string) (map[string]string, error) { 20 | claims, err := ed.alg.Decode(token) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | for key := range kvs { 26 | iVal, err := claims.Get(key) 27 | if err != nil { 28 | return nil, err 29 | } 30 | strVal, ok := iVal.(string) 31 | if !ok { 32 | return nil, errors.New("incorrect JWT claim") 33 | } 34 | 35 | kvs[key] = strVal 36 | } 37 | return kvs, nil 38 | } 39 | 40 | func (ed *JWTEncDec) ToToken(kvs map[string]string) (string, error) { 41 | claims := jwtpkg.NewClaim() 42 | for key, val := range kvs { 43 | claims.Set(key, val) 44 | } 45 | 46 | token, err := ed.alg.Encode(claims) 47 | if err != nil { 48 | return "", err 49 | } 50 | return token, nil 51 | } 52 | -------------------------------------------------------------------------------- /src/db/interfaces.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | _ "github.com/mattn/go-sqlite3" 8 | ) 9 | 10 | // TODO: expose more APIs if needed 11 | type IDB interface { 12 | BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) 13 | Close() error 14 | PingContext(ctx context.Context) error 15 | PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 16 | ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) 17 | QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) 18 | QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row 19 | // Conn(ctx context.Context) (*Conn, error) 20 | // Driver() driver.Driver 21 | // SetConnMaxIdleTime(d time.Duration) 22 | // SetConnMaxLifetime(d time.Duration) 23 | // SetMaxIdleConns(n int) 24 | // SetMaxOpenConns(n int) 25 | // Stats() DBStats 26 | } 27 | 28 | type IDBQuickshare interface { 29 | Init(ctx context.Context, adminName, adminPwd string, config *SiteConfig) error 30 | InitUserTable(ctx context.Context, tx *sql.Tx, rootName, rootPwd string) error 31 | InitFileTables(ctx context.Context, tx *sql.Tx) error 32 | InitConfigTable(ctx context.Context, tx *sql.Tx, cfg *SiteConfig) error 33 | Close() error 34 | IDBLockable 35 | IUserDB 36 | IFileDB 37 | IUploadDB 38 | ISharingDB 39 | IConfigDB 40 | } 41 | 42 | type IDBLockable interface { 43 | Lock() 44 | Unlock() 45 | RLock() 46 | RUnlock() 47 | } 48 | 49 | type IUserDB interface { 50 | AddUser(ctx context.Context, user *User) error 51 | DelUser(ctx context.Context, id uint64) error 52 | GetUser(ctx context.Context, id uint64) (*User, error) 53 | GetUserByName(ctx context.Context, name string) (*User, error) 54 | SetPwd(ctx context.Context, id uint64, pwd string) error 55 | SetInfo(ctx context.Context, id uint64, user *User) error 56 | SetPreferences(ctx context.Context, id uint64, prefers *Preferences) error 57 | SetUsed(ctx context.Context, id uint64, incr bool, capacity int64) error 58 | ResetUsed(ctx context.Context, id uint64, used int64) error 59 | ListUsers(ctx context.Context) ([]*User, error) 60 | ListUserIDs(ctx context.Context) (map[string]string, error) 61 | AddRole(role string) error 62 | DelRole(role string) error 63 | ListRoles() (map[string]bool, error) 64 | } 65 | 66 | type IFilesFunctions interface { 67 | IFileDB 68 | IUploadDB 69 | ISharingDB 70 | } 71 | 72 | type IFileDB interface { 73 | AddFileInfo(ctx context.Context, infoId, userId uint64, itemPath string, info *FileInfo) error 74 | DelFileInfo(ctx context.Context, userId uint64, itemPath string) error 75 | GetFileInfo(ctx context.Context, itemPath string) (*FileInfo, error) 76 | SetSha1(ctx context.Context, itemPath, sign string) error 77 | MoveFileInfo(ctx context.Context, userId uint64, oldPath, newPath string, isDir bool) error 78 | ListFileInfos(ctx context.Context, itemPaths []string) (map[string]*FileInfo, error) 79 | } 80 | type IUploadDB interface { 81 | AddUploadInfos(ctx context.Context, uploadId, userId uint64, tmpPath, filePath string, info *FileInfo) error 82 | DelUploadingInfos(ctx context.Context, userId uint64, realPath string) error 83 | MoveUploadingInfos(ctx context.Context, uploadId, userId uint64, uploadPath, itemPath string) error 84 | SetUploadInfo(ctx context.Context, user uint64, filePath string, newUploaded int64) error 85 | GetUploadInfo(ctx context.Context, userId uint64, filePath string) (string, int64, int64, error) 86 | ListUploadInfos(ctx context.Context, user uint64) ([]*UploadInfo, error) 87 | } 88 | 89 | type ISharingDB interface { 90 | IsSharing(ctx context.Context, dirPath string) (bool, error) 91 | GetSharingDir(ctx context.Context, hashID string) (string, error) 92 | AddSharing(ctx context.Context, infoId, userId uint64, dirPath string) error 93 | DelSharing(ctx context.Context, userId uint64, dirPath string) error 94 | ListSharingsByLocation(ctx context.Context, location string) (map[string]string, error) 95 | } 96 | 97 | type IConfigDB interface { 98 | SetClientCfg(ctx context.Context, cfg *ClientConfig) error 99 | GetCfg(ctx context.Context) (*SiteConfig, error) 100 | } 101 | -------------------------------------------------------------------------------- /src/db/rdb/base/configs.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | 8 | "github.com/ihexxa/quickshare/src/db" 9 | ) 10 | 11 | func (st *BaseStore) getCfg(ctx context.Context, tx *sql.Tx) (*db.SiteConfig, error) { 12 | var configStr string 13 | err := tx.QueryRowContext( 14 | ctx, 15 | `select config 16 | from t_config 17 | where id=0`, 18 | ).Scan(&configStr) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | config := &db.SiteConfig{} 24 | err = json.Unmarshal([]byte(configStr), config) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | if err = db.CheckSiteCfg(config, true); err != nil { 30 | return nil, err 31 | } 32 | return config, nil 33 | } 34 | 35 | func (st *BaseStore) setCfg(ctx context.Context, tx *sql.Tx, cfg *db.SiteConfig) error { 36 | if err := db.CheckSiteCfg(cfg, false); err != nil { 37 | return err 38 | } 39 | 40 | cfgBytes, err := json.Marshal(cfg) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | _, err = tx.ExecContext( 46 | ctx, 47 | `update t_config 48 | set config=? 49 | where id=0`, 50 | string(cfgBytes), 51 | ) 52 | return err 53 | } 54 | 55 | func (st *BaseStore) SetClientCfg(ctx context.Context, cfg *db.ClientConfig) error { 56 | tx, err := st.db.BeginTx(ctx, txOpts) 57 | if err != nil { 58 | return err 59 | } 60 | defer tx.Rollback() 61 | 62 | siteCfg, err := st.getCfg(ctx, tx) 63 | if err != nil { 64 | return err 65 | } 66 | siteCfg.ClientCfg = cfg 67 | 68 | err = st.setCfg(ctx, tx, siteCfg) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | return tx.Commit() 74 | } 75 | 76 | func (st *BaseStore) GetCfg(ctx context.Context) (*db.SiteConfig, error) { 77 | tx, err := st.db.BeginTx(ctx, txOpts) 78 | if err != nil { 79 | return nil, err 80 | } 81 | defer tx.Rollback() 82 | 83 | siteConfig, err := st.getCfg(ctx, tx) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | err = tx.Commit() 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | return siteConfig, nil 94 | } 95 | -------------------------------------------------------------------------------- /src/db/rdb/sqlite/configs.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ihexxa/quickshare/src/db" 7 | ) 8 | 9 | func (st *SQLiteStore) SetClientCfg(ctx context.Context, cfg *db.ClientConfig) error { 10 | st.Lock() 11 | defer st.Unlock() 12 | 13 | return st.store.SetClientCfg(ctx, cfg) 14 | } 15 | 16 | func (st *SQLiteStore) GetCfg(ctx context.Context) (*db.SiteConfig, error) { 17 | st.RLock() 18 | defer st.RUnlock() 19 | 20 | return st.store.GetCfg(ctx) 21 | } 22 | -------------------------------------------------------------------------------- /src/db/rdb/sqlite/files.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ihexxa/quickshare/src/db" 7 | ) 8 | 9 | func (st *SQLiteStore) GetFileInfo(ctx context.Context, itemPath string) (*db.FileInfo, error) { 10 | st.RLock() 11 | defer st.RUnlock() 12 | 13 | return st.store.GetFileInfo(ctx, itemPath) 14 | } 15 | 16 | func (st *SQLiteStore) ListFileInfos(ctx context.Context, itemPaths []string) (map[string]*db.FileInfo, error) { 17 | st.RLock() 18 | defer st.RUnlock() 19 | 20 | return st.store.ListFileInfos(ctx, itemPaths) 21 | } 22 | 23 | func (st *SQLiteStore) AddFileInfo(ctx context.Context, infoId, userId uint64, itemPath string, info *db.FileInfo) error { 24 | st.Lock() 25 | defer st.Unlock() 26 | 27 | return st.store.AddFileInfo(ctx, infoId, userId, itemPath, info) 28 | } 29 | 30 | func (st *SQLiteStore) SetSha1(ctx context.Context, itemPath, sign string) error { 31 | st.Lock() 32 | defer st.Unlock() 33 | 34 | return st.store.SetSha1(ctx, itemPath, sign) 35 | } 36 | 37 | func (st *SQLiteStore) DelFileInfo(ctx context.Context, userID uint64, itemPath string) error { 38 | st.Lock() 39 | defer st.Unlock() 40 | 41 | return st.store.DelFileInfo(ctx, userID, itemPath) 42 | } 43 | 44 | func (st *SQLiteStore) MoveFileInfo(ctx context.Context, userId uint64, oldPath, newPath string, isDir bool) error { 45 | st.Lock() 46 | defer st.Unlock() 47 | 48 | return st.store.MoveFileInfo(ctx, userId, oldPath, newPath, isDir) 49 | } 50 | -------------------------------------------------------------------------------- /src/db/rdb/sqlite/files_sharings.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func (st *SQLiteStore) IsSharing(ctx context.Context, dirPath string) (bool, error) { 8 | st.RLock() 9 | defer st.RUnlock() 10 | 11 | return st.store.IsSharing(ctx, dirPath) 12 | } 13 | 14 | func (st *SQLiteStore) GetSharingDir(ctx context.Context, hashID string) (string, error) { 15 | st.RLock() 16 | defer st.RUnlock() 17 | 18 | return st.store.GetSharingDir(ctx, hashID) 19 | } 20 | 21 | func (st *SQLiteStore) AddSharing(ctx context.Context, infoId, userId uint64, dirPath string) error { 22 | st.Lock() 23 | defer st.Unlock() 24 | 25 | return st.store.AddSharing(ctx, infoId, userId, dirPath) 26 | } 27 | 28 | func (st *SQLiteStore) DelSharing(ctx context.Context, userId uint64, dirPath string) error { 29 | st.Lock() 30 | defer st.Unlock() 31 | 32 | return st.store.DelSharing(ctx, userId, dirPath) 33 | } 34 | 35 | func (st *SQLiteStore) ListSharingsByLocation(ctx context.Context, location string) (map[string]string, error) { 36 | st.RLock() 37 | defer st.RUnlock() 38 | 39 | return st.store.ListSharingsByLocation(ctx, location) 40 | } 41 | -------------------------------------------------------------------------------- /src/db/rdb/sqlite/files_uploadings.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ihexxa/quickshare/src/db" 7 | ) 8 | 9 | func (st *SQLiteStore) AddUploadInfos(ctx context.Context, uploadId, userId uint64, tmpPath, filePath string, info *db.FileInfo) error { 10 | st.Lock() 11 | defer st.Unlock() 12 | 13 | return st.store.AddUploadInfos(ctx, uploadId, userId, tmpPath, filePath, info) 14 | } 15 | 16 | func (st *SQLiteStore) DelUploadingInfos(ctx context.Context, userId uint64, realPath string) error { 17 | st.Lock() 18 | defer st.Unlock() 19 | 20 | return st.store.DelUploadingInfos(ctx, userId, realPath) 21 | } 22 | 23 | func (st *SQLiteStore) MoveUploadingInfos(ctx context.Context, infoId, userId uint64, uploadPath, itemPath string) error { 24 | st.Lock() 25 | defer st.Unlock() 26 | 27 | return st.store.MoveUploadingInfos(ctx, infoId, userId, uploadPath, itemPath) 28 | } 29 | 30 | func (st *SQLiteStore) SetUploadInfo(ctx context.Context, userId uint64, filePath string, newUploaded int64) error { 31 | st.Lock() 32 | defer st.Unlock() 33 | 34 | return st.store.SetUploadInfo(ctx, userId, filePath, newUploaded) 35 | } 36 | 37 | func (st *SQLiteStore) GetUploadInfo(ctx context.Context, userId uint64, filePath string) (string, int64, int64, error) { 38 | st.RLock() 39 | defer st.RUnlock() 40 | 41 | return st.store.GetUploadInfo(ctx, userId, filePath) 42 | } 43 | 44 | func (st *SQLiteStore) ListUploadInfos(ctx context.Context, userId uint64) ([]*db.UploadInfo, error) { 45 | st.RLock() 46 | defer st.RUnlock() 47 | 48 | return st.store.ListUploadInfos(ctx, userId) 49 | } 50 | -------------------------------------------------------------------------------- /src/db/rdb/sqlite/init.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "sync" 7 | 8 | "github.com/ihexxa/quickshare/src/db" 9 | "github.com/ihexxa/quickshare/src/db/rdb/base" 10 | _ "modernc.org/sqlite" 11 | ) 12 | 13 | type SQLite struct { 14 | db.IDB 15 | dbPath string 16 | } 17 | 18 | func NewSQLite(dbPath string) (*SQLite, error) { 19 | db, err := sql.Open("sqlite", dbPath) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return &SQLite{ 25 | IDB: db, 26 | dbPath: dbPath, 27 | }, nil 28 | } 29 | 30 | type SQLiteStore struct { 31 | store *base.BaseStore 32 | mtx *sync.RWMutex 33 | } 34 | 35 | func NewSQLiteStore(db db.IDB) (*SQLiteStore, error) { 36 | return &SQLiteStore{ 37 | store: base.NewBaseStore(db), 38 | mtx: &sync.RWMutex{}, 39 | }, nil 40 | } 41 | 42 | func (st *SQLiteStore) Close() error { 43 | return st.store.Close() 44 | } 45 | 46 | func (st *SQLiteStore) Lock() { 47 | st.mtx.Lock() 48 | } 49 | 50 | func (st *SQLiteStore) Unlock() { 51 | st.mtx.Unlock() 52 | } 53 | 54 | func (st *SQLiteStore) RLock() { 55 | st.mtx.RLock() 56 | } 57 | 58 | func (st *SQLiteStore) RUnlock() { 59 | st.mtx.RUnlock() 60 | } 61 | 62 | func (st *SQLiteStore) IsInited() bool { 63 | // always try to init the db 64 | return false 65 | } 66 | 67 | func (st *SQLiteStore) Init(ctx context.Context, rootName, rootPwd string, cfg *db.SiteConfig) error { 68 | st.Lock() 69 | defer st.Unlock() 70 | 71 | return st.store.Init(ctx, rootName, rootPwd, cfg) 72 | } 73 | 74 | func (st *SQLiteStore) InitUserTable(ctx context.Context, tx *sql.Tx, rootName, rootPwd string) error { 75 | return st.store.InitUserTable(ctx, tx, rootName, rootPwd) 76 | } 77 | 78 | func (st *SQLiteStore) InitFileTables(ctx context.Context, tx *sql.Tx) error { 79 | return st.store.InitFileTables(ctx, tx) 80 | } 81 | 82 | func (st *SQLiteStore) InitConfigTable(ctx context.Context, tx *sql.Tx, cfg *db.SiteConfig) error { 83 | return st.store.InitConfigTable(ctx, tx, cfg) 84 | } 85 | -------------------------------------------------------------------------------- /src/db/rdb/sqlite/users.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ihexxa/quickshare/src/db" 7 | ) 8 | 9 | func (st *SQLiteStore) AddUser(ctx context.Context, user *db.User) error { 10 | st.Lock() 11 | defer st.Unlock() 12 | 13 | return st.store.AddUser(ctx, user) 14 | } 15 | 16 | func (st *SQLiteStore) DelUser(ctx context.Context, id uint64) error { 17 | st.Lock() 18 | defer st.Unlock() 19 | 20 | return st.store.DelUser(ctx, id) 21 | } 22 | 23 | func (st *SQLiteStore) GetUser(ctx context.Context, id uint64) (*db.User, error) { 24 | st.RLock() 25 | defer st.RUnlock() 26 | 27 | return st.store.GetUser(ctx, id) 28 | } 29 | 30 | func (st *SQLiteStore) GetUserByName(ctx context.Context, name string) (*db.User, error) { 31 | st.RLock() 32 | defer st.RUnlock() 33 | 34 | return st.store.GetUserByName(ctx, name) 35 | } 36 | 37 | func (st *SQLiteStore) SetPwd(ctx context.Context, id uint64, pwd string) error { 38 | st.Lock() 39 | defer st.Unlock() 40 | 41 | return st.store.SetPwd(ctx, id, pwd) 42 | } 43 | 44 | // role + quota 45 | func (st *SQLiteStore) SetInfo(ctx context.Context, id uint64, user *db.User) error { 46 | st.Lock() 47 | defer st.Unlock() 48 | 49 | return st.store.SetInfo(ctx, id, user) 50 | } 51 | 52 | func (st *SQLiteStore) SetPreferences(ctx context.Context, id uint64, prefers *db.Preferences) error { 53 | st.Lock() 54 | defer st.Unlock() 55 | 56 | return st.store.SetPreferences(ctx, id, prefers) 57 | } 58 | 59 | func (st *SQLiteStore) SetUsed(ctx context.Context, id uint64, incr bool, capacity int64) error { 60 | st.Lock() 61 | defer st.Unlock() 62 | 63 | return st.store.SetUsed(ctx, id, incr, capacity) 64 | } 65 | 66 | func (st *SQLiteStore) ResetUsed(ctx context.Context, id uint64, used int64) error { 67 | st.Lock() 68 | defer st.Unlock() 69 | 70 | return st.store.ResetUsed(ctx, id, used) 71 | } 72 | 73 | func (st *SQLiteStore) ListUsers(ctx context.Context) ([]*db.User, error) { 74 | st.RLock() 75 | defer st.RUnlock() 76 | 77 | return st.store.ListUsers(ctx) 78 | } 79 | 80 | func (st *SQLiteStore) ListUserIDs(ctx context.Context) (map[string]string, error) { 81 | st.RLock() 82 | defer st.RUnlock() 83 | 84 | return st.store.ListUserIDs(ctx) 85 | } 86 | 87 | func (st *SQLiteStore) AddRole(role string) error { 88 | // TODO: implement this after adding grant/revoke 89 | panic("not implemented") 90 | } 91 | 92 | func (st *SQLiteStore) DelRole(role string) error { 93 | // TODO: implement this after adding grant/revoke 94 | panic("not implemented") 95 | } 96 | 97 | func (st *SQLiteStore) ListRoles() (map[string]bool, error) { 98 | // TODO: implement this after adding grant/revoke 99 | panic("not implemented") 100 | } 101 | -------------------------------------------------------------------------------- /src/db/rdb/sqlitecgo/configs.go: -------------------------------------------------------------------------------- 1 | package sqlitecgo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ihexxa/quickshare/src/db" 7 | ) 8 | 9 | func (st *SQLiteStore) SetClientCfg(ctx context.Context, cfg *db.ClientConfig) error { 10 | st.Lock() 11 | defer st.Unlock() 12 | 13 | return st.store.SetClientCfg(ctx, cfg) 14 | } 15 | 16 | func (st *SQLiteStore) GetCfg(ctx context.Context) (*db.SiteConfig, error) { 17 | st.RLock() 18 | defer st.RUnlock() 19 | 20 | return st.store.GetCfg(ctx) 21 | } 22 | -------------------------------------------------------------------------------- /src/db/rdb/sqlitecgo/files.go: -------------------------------------------------------------------------------- 1 | package sqlitecgo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ihexxa/quickshare/src/db" 7 | ) 8 | 9 | func (st *SQLiteStore) GetFileInfo(ctx context.Context, itemPath string) (*db.FileInfo, error) { 10 | st.RLock() 11 | defer st.RUnlock() 12 | 13 | return st.store.GetFileInfo(ctx, itemPath) 14 | } 15 | 16 | func (st *SQLiteStore) ListFileInfos(ctx context.Context, itemPaths []string) (map[string]*db.FileInfo, error) { 17 | st.RLock() 18 | defer st.RUnlock() 19 | 20 | return st.store.ListFileInfos(ctx, itemPaths) 21 | } 22 | 23 | func (st *SQLiteStore) AddFileInfo(ctx context.Context, infoId, userId uint64, itemPath string, info *db.FileInfo) error { 24 | st.Lock() 25 | defer st.Unlock() 26 | 27 | return st.store.AddFileInfo(ctx, infoId, userId, itemPath, info) 28 | } 29 | 30 | func (st *SQLiteStore) SetSha1(ctx context.Context, itemPath, sign string) error { 31 | st.Lock() 32 | defer st.Unlock() 33 | 34 | return st.store.SetSha1(ctx, itemPath, sign) 35 | } 36 | 37 | func (st *SQLiteStore) DelFileInfo(ctx context.Context, userID uint64, itemPath string) error { 38 | st.Lock() 39 | defer st.Unlock() 40 | 41 | return st.store.DelFileInfo(ctx, userID, itemPath) 42 | } 43 | 44 | func (st *SQLiteStore) MoveFileInfo(ctx context.Context, userId uint64, oldPath, newPath string, isDir bool) error { 45 | st.Lock() 46 | defer st.Unlock() 47 | 48 | return st.store.MoveFileInfo(ctx, userId, oldPath, newPath, isDir) 49 | } 50 | -------------------------------------------------------------------------------- /src/db/rdb/sqlitecgo/files_sharings.go: -------------------------------------------------------------------------------- 1 | package sqlitecgo 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func (st *SQLiteStore) IsSharing(ctx context.Context, dirPath string) (bool, error) { 8 | st.RLock() 9 | defer st.RUnlock() 10 | 11 | return st.store.IsSharing(ctx, dirPath) 12 | } 13 | 14 | func (st *SQLiteStore) GetSharingDir(ctx context.Context, hashID string) (string, error) { 15 | st.RLock() 16 | defer st.RUnlock() 17 | 18 | return st.store.GetSharingDir(ctx, hashID) 19 | } 20 | 21 | func (st *SQLiteStore) AddSharing(ctx context.Context, infoId, userId uint64, dirPath string) error { 22 | st.Lock() 23 | defer st.Unlock() 24 | 25 | return st.store.AddSharing(ctx, infoId, userId, dirPath) 26 | } 27 | 28 | func (st *SQLiteStore) DelSharing(ctx context.Context, userId uint64, dirPath string) error { 29 | st.Lock() 30 | defer st.Unlock() 31 | 32 | return st.store.DelSharing(ctx, userId, dirPath) 33 | } 34 | 35 | func (st *SQLiteStore) ListSharingsByLocation(ctx context.Context, location string) (map[string]string, error) { 36 | st.RLock() 37 | defer st.RUnlock() 38 | 39 | return st.store.ListSharingsByLocation(ctx, location) 40 | } 41 | -------------------------------------------------------------------------------- /src/db/rdb/sqlitecgo/files_uploadings.go: -------------------------------------------------------------------------------- 1 | package sqlitecgo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ihexxa/quickshare/src/db" 7 | ) 8 | 9 | func (st *SQLiteStore) AddUploadInfos(ctx context.Context, uploadId, userId uint64, tmpPath, filePath string, info *db.FileInfo) error { 10 | st.Lock() 11 | defer st.Unlock() 12 | 13 | return st.store.AddUploadInfos(ctx, uploadId, userId, tmpPath, filePath, info) 14 | } 15 | 16 | func (st *SQLiteStore) DelUploadingInfos(ctx context.Context, userId uint64, realPath string) error { 17 | st.Lock() 18 | defer st.Unlock() 19 | 20 | return st.store.DelUploadingInfos(ctx, userId, realPath) 21 | } 22 | 23 | func (st *SQLiteStore) MoveUploadingInfos(ctx context.Context, infoId, userId uint64, uploadPath, itemPath string) error { 24 | st.Lock() 25 | defer st.Unlock() 26 | 27 | return st.store.MoveUploadingInfos(ctx, infoId, userId, uploadPath, itemPath) 28 | } 29 | 30 | func (st *SQLiteStore) SetUploadInfo(ctx context.Context, userId uint64, filePath string, newUploaded int64) error { 31 | st.Lock() 32 | defer st.Unlock() 33 | 34 | return st.store.SetUploadInfo(ctx, userId, filePath, newUploaded) 35 | } 36 | 37 | func (st *SQLiteStore) GetUploadInfo(ctx context.Context, userId uint64, filePath string) (string, int64, int64, error) { 38 | st.RLock() 39 | defer st.RUnlock() 40 | 41 | return st.store.GetUploadInfo(ctx, userId, filePath) 42 | } 43 | 44 | func (st *SQLiteStore) ListUploadInfos(ctx context.Context, userId uint64) ([]*db.UploadInfo, error) { 45 | st.RLock() 46 | defer st.RUnlock() 47 | 48 | return st.store.ListUploadInfos(ctx, userId) 49 | } 50 | -------------------------------------------------------------------------------- /src/db/rdb/sqlitecgo/init.go: -------------------------------------------------------------------------------- 1 | package sqlitecgo 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "sync" 7 | 8 | "github.com/ihexxa/quickshare/src/db" 9 | "github.com/ihexxa/quickshare/src/db/rdb/base" 10 | _ "modernc.org/sqlite" 11 | ) 12 | 13 | type SQLite struct { 14 | db.IDB 15 | dbPath string 16 | } 17 | 18 | func NewSQLite(dbPath string) (*SQLite, error) { 19 | db, err := sql.Open("sqlite", dbPath) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return &SQLite{ 25 | IDB: db, 26 | dbPath: dbPath, 27 | }, nil 28 | } 29 | 30 | type SQLiteStore struct { 31 | store *base.BaseStore 32 | mtx *sync.RWMutex 33 | } 34 | 35 | func NewSQLiteStore(db db.IDB) (*SQLiteStore, error) { 36 | return &SQLiteStore{ 37 | store: base.NewBaseStore(db), 38 | mtx: &sync.RWMutex{}, 39 | }, nil 40 | } 41 | 42 | func (st *SQLiteStore) Close() error { 43 | return st.store.Close() 44 | } 45 | 46 | func (st *SQLiteStore) Lock() { 47 | st.mtx.Lock() 48 | } 49 | 50 | func (st *SQLiteStore) Unlock() { 51 | st.mtx.Unlock() 52 | } 53 | 54 | func (st *SQLiteStore) RLock() { 55 | st.mtx.RLock() 56 | } 57 | 58 | func (st *SQLiteStore) RUnlock() { 59 | st.mtx.RUnlock() 60 | } 61 | 62 | func (st *SQLiteStore) IsInited() bool { 63 | // always try to init the db 64 | return false 65 | } 66 | 67 | func (st *SQLiteStore) Init(ctx context.Context, rootName, rootPwd string, cfg *db.SiteConfig) error { 68 | st.Lock() 69 | defer st.Unlock() 70 | 71 | return st.store.Init(ctx, rootName, rootPwd, cfg) 72 | } 73 | 74 | func (st *SQLiteStore) InitUserTable(ctx context.Context, tx *sql.Tx, rootName, rootPwd string) error { 75 | return st.store.InitUserTable(ctx, tx, rootName, rootPwd) 76 | } 77 | 78 | func (st *SQLiteStore) InitFileTables(ctx context.Context, tx *sql.Tx) error { 79 | return st.store.InitFileTables(ctx, tx) 80 | } 81 | 82 | func (st *SQLiteStore) InitConfigTable(ctx context.Context, tx *sql.Tx, cfg *db.SiteConfig) error { 83 | return st.store.InitConfigTable(ctx, tx, cfg) 84 | } 85 | -------------------------------------------------------------------------------- /src/db/rdb/sqlitecgo/users.go: -------------------------------------------------------------------------------- 1 | package sqlitecgo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ihexxa/quickshare/src/db" 7 | ) 8 | 9 | func (st *SQLiteStore) AddUser(ctx context.Context, user *db.User) error { 10 | st.Lock() 11 | defer st.Unlock() 12 | 13 | return st.store.AddUser(ctx, user) 14 | } 15 | 16 | func (st *SQLiteStore) DelUser(ctx context.Context, id uint64) error { 17 | st.Lock() 18 | defer st.Unlock() 19 | 20 | return st.store.DelUser(ctx, id) 21 | } 22 | 23 | func (st *SQLiteStore) GetUser(ctx context.Context, id uint64) (*db.User, error) { 24 | st.RLock() 25 | defer st.RUnlock() 26 | 27 | return st.store.GetUser(ctx, id) 28 | } 29 | 30 | func (st *SQLiteStore) GetUserByName(ctx context.Context, name string) (*db.User, error) { 31 | st.RLock() 32 | defer st.RUnlock() 33 | 34 | return st.store.GetUserByName(ctx, name) 35 | } 36 | 37 | func (st *SQLiteStore) SetPwd(ctx context.Context, id uint64, pwd string) error { 38 | st.Lock() 39 | defer st.Unlock() 40 | 41 | return st.store.SetPwd(ctx, id, pwd) 42 | } 43 | 44 | // role + quota 45 | func (st *SQLiteStore) SetInfo(ctx context.Context, id uint64, user *db.User) error { 46 | st.Lock() 47 | defer st.Unlock() 48 | 49 | return st.store.SetInfo(ctx, id, user) 50 | } 51 | 52 | func (st *SQLiteStore) SetPreferences(ctx context.Context, id uint64, prefers *db.Preferences) error { 53 | st.Lock() 54 | defer st.Unlock() 55 | 56 | return st.store.SetPreferences(ctx, id, prefers) 57 | } 58 | 59 | func (st *SQLiteStore) SetUsed(ctx context.Context, id uint64, incr bool, capacity int64) error { 60 | st.Lock() 61 | defer st.Unlock() 62 | 63 | return st.store.SetUsed(ctx, id, incr, capacity) 64 | } 65 | 66 | func (st *SQLiteStore) ResetUsed(ctx context.Context, id uint64, used int64) error { 67 | st.Lock() 68 | defer st.Unlock() 69 | 70 | return st.store.ResetUsed(ctx, id, used) 71 | } 72 | 73 | func (st *SQLiteStore) ListUsers(ctx context.Context) ([]*db.User, error) { 74 | st.RLock() 75 | defer st.RUnlock() 76 | 77 | return st.store.ListUsers(ctx) 78 | } 79 | 80 | func (st *SQLiteStore) ListUserIDs(ctx context.Context) (map[string]string, error) { 81 | st.RLock() 82 | defer st.RUnlock() 83 | 84 | return st.store.ListUserIDs(ctx) 85 | } 86 | 87 | func (st *SQLiteStore) AddRole(role string) error { 88 | // TODO: implement this after adding grant/revoke 89 | panic("not implemented") 90 | } 91 | 92 | func (st *SQLiteStore) DelRole(role string) error { 93 | // TODO: implement this after adding grant/revoke 94 | panic("not implemented") 95 | } 96 | 97 | func (st *SQLiteStore) ListRoles() (map[string]bool, error) { 98 | // TODO: implement this after adding grant/revoke 99 | panic("not implemented") 100 | } 101 | -------------------------------------------------------------------------------- /src/db/tests/common_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/ihexxa/quickshare/src/db/rdb/sqlite" 11 | ) 12 | 13 | func TestSqliteInit(t *testing.T) { 14 | t.Run("idemptent initialization - sqlite", func(t *testing.T) { 15 | rootPath, err := ioutil.TempDir("./", "qs_sqlite_config_") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | defer os.RemoveAll(rootPath) 20 | 21 | dbPath := filepath.Join(rootPath, "quickshare.sqlite") 22 | sqliteDB, err := sqlite.NewSQLite(dbPath) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | defer sqliteDB.Close() 27 | 28 | store, err := sqlite.NewSQLiteStore(sqliteDB) 29 | if err != nil { 30 | t.Fatal("fail to new sqlite store", err) 31 | } 32 | 33 | for i := 0; i < 2; i++ { 34 | err = store.Init(context.TODO(), "admin", "adminPwd", testSiteConfig) 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/db/tests/config_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/ihexxa/quickshare/src/db" 12 | "github.com/ihexxa/quickshare/src/db/rdb/sqlite" 13 | ) 14 | 15 | var testSiteConfig = &db.SiteConfig{ 16 | ClientCfg: &db.ClientConfig{ 17 | SiteName: "", 18 | SiteDesc: "", 19 | AllowSetBg: true, 20 | AutoTheme: false, 21 | Bg: &db.BgConfig{ 22 | Url: "/imgs/bg.jpg", 23 | Repeat: "repeat", 24 | Position: "top", 25 | Align: "scroll", 26 | BgColor: "#000", 27 | }, 28 | }, 29 | } 30 | 31 | func TestSiteStore(t *testing.T) { 32 | testConfigMethods := func(t *testing.T, store db.IConfigDB) { 33 | siteCfg := &db.SiteConfig{ 34 | ClientCfg: &db.ClientConfig{ 35 | SiteName: "quickshare", 36 | SiteDesc: "simpel file sharing", 37 | AllowSetBg: true, 38 | AutoTheme: true, 39 | Bg: &db.BgConfig{ 40 | Url: "/imgs/bg.jpg", 41 | Repeat: "no-repeat", 42 | Position: "center", 43 | Align: "fixed", 44 | BgColor: "#ccc", 45 | }, 46 | }, 47 | } 48 | 49 | ctx := context.TODO() 50 | err := store.SetClientCfg(ctx, siteCfg.ClientCfg) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | newSiteCfg, err := store.GetCfg(ctx) 55 | if err != nil { 56 | t.Fatal(err) 57 | } else if !reflect.DeepEqual(newSiteCfg, siteCfg) { 58 | t.Fatalf("not equal new(%v) original(%v)", newSiteCfg, siteCfg) 59 | } 60 | } 61 | 62 | t.Run("config methods basic tests - sqlite", func(t *testing.T) { 63 | rootPath, err := ioutil.TempDir("./", "qs_sqlite_config_") 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | defer os.RemoveAll(rootPath) 68 | 69 | dbPath := filepath.Join(rootPath, "quickshare.sqlite") 70 | sqliteDB, err := sqlite.NewSQLite(dbPath) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | defer sqliteDB.Close() 75 | 76 | store, err := sqlite.NewSQLiteStore(sqliteDB) 77 | if err != nil { 78 | t.Fatal("fail to new sqlite store", err) 79 | } 80 | err = store.Init(context.TODO(), "admin", "adminPwd", testSiteConfig) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | testConfigMethods(t, store) 86 | }) 87 | 88 | t.Run("idemptent initialization - sqlite", func(t *testing.T) { 89 | rootPath, err := ioutil.TempDir("./", "qs_sqlite_config_") 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | defer os.RemoveAll(rootPath) 94 | 95 | dbPath := filepath.Join(rootPath, "quickshare.sqlite") 96 | sqliteDB, err := sqlite.NewSQLite(dbPath) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | defer sqliteDB.Close() 101 | 102 | store, err := sqlite.NewSQLiteStore(sqliteDB) 103 | if err != nil { 104 | t.Fatal("fail to new sqlite store", err) 105 | } 106 | 107 | for i := 0; i < 2; i++ { 108 | err = store.Init(context.TODO(), "admin", "adminPwd", testSiteConfig) 109 | if err != nil { 110 | panic(err) 111 | } 112 | } 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /src/depidx/deps.go: -------------------------------------------------------------------------------- 1 | package depidx 2 | 3 | import ( 4 | "github.com/ihexxa/gocfg" 5 | "go.uber.org/zap" 6 | 7 | "github.com/ihexxa/quickshare/src/cron" 8 | "github.com/ihexxa/quickshare/src/cryptoutil" 9 | "github.com/ihexxa/quickshare/src/db" 10 | "github.com/ihexxa/quickshare/src/fs" 11 | "github.com/ihexxa/quickshare/src/idgen" 12 | "github.com/ihexxa/quickshare/src/iolimiter" 13 | "github.com/ihexxa/quickshare/src/kvstore" 14 | "github.com/ihexxa/quickshare/src/search/fileindex" 15 | "github.com/ihexxa/quickshare/src/worker" 16 | ) 17 | 18 | type IUploader interface { 19 | Create(filePath string, size int64) error 20 | WriteChunk(filePath string, chunk []byte, off int64) (int, error) 21 | Status(filePath string) (int64, bool, error) 22 | Close() error 23 | Sync() error 24 | } 25 | 26 | type Deps struct { 27 | fs fs.ISimpleFS 28 | token cryptoutil.ITokenEncDec 29 | kv kvstore.IKVStore 30 | id idgen.IIDGen 31 | logger *zap.SugaredLogger 32 | limiter iolimiter.ILimiter 33 | workers worker.IWorkerPool 34 | cron cron.ICron 35 | fileIndex fileindex.IFileIndex 36 | db db.IDBQuickshare 37 | } 38 | 39 | func NewDeps(cfg gocfg.ICfg) *Deps { 40 | return &Deps{} 41 | } 42 | 43 | func (deps *Deps) FS() fs.ISimpleFS { 44 | return deps.fs 45 | } 46 | 47 | func (deps *Deps) SetFS(filesystem fs.ISimpleFS) { 48 | deps.fs = filesystem 49 | } 50 | 51 | func (deps *Deps) Token() cryptoutil.ITokenEncDec { 52 | return deps.token 53 | } 54 | 55 | func (deps *Deps) SetToken(tokenMaker cryptoutil.ITokenEncDec) { 56 | deps.token = tokenMaker 57 | } 58 | 59 | func (deps *Deps) KV() kvstore.IKVStore { 60 | return deps.kv 61 | } 62 | 63 | func (deps *Deps) SetKV(kvstore kvstore.IKVStore) { 64 | deps.kv = kvstore 65 | } 66 | 67 | func (deps *Deps) ID() idgen.IIDGen { 68 | return deps.id 69 | } 70 | 71 | func (deps *Deps) SetID(ider idgen.IIDGen) { 72 | deps.id = ider 73 | } 74 | 75 | func (deps *Deps) Log() *zap.SugaredLogger { 76 | return deps.logger 77 | } 78 | 79 | func (deps *Deps) SetLog(logger *zap.SugaredLogger) { 80 | deps.logger = logger 81 | } 82 | 83 | func (deps *Deps) Users() db.IUserDB { 84 | return deps.db 85 | } 86 | 87 | func (deps *Deps) FileInfos() db.IFilesFunctions { 88 | return deps.db 89 | } 90 | 91 | func (deps *Deps) SiteStore() db.IConfigDB { 92 | return deps.db 93 | } 94 | 95 | func (deps *Deps) Limiter() iolimiter.ILimiter { 96 | return deps.limiter 97 | } 98 | 99 | func (deps *Deps) SetLimiter(limiter iolimiter.ILimiter) { 100 | deps.limiter = limiter 101 | } 102 | 103 | func (deps *Deps) Workers() worker.IWorkerPool { 104 | return deps.workers 105 | } 106 | 107 | func (deps *Deps) SetWorkers(workers worker.IWorkerPool) { 108 | deps.workers = workers 109 | } 110 | 111 | func (deps *Deps) Cron() cron.ICron { 112 | return deps.cron 113 | } 114 | 115 | func (deps *Deps) SetCron(cronImp cron.ICron) { 116 | deps.cron = cronImp 117 | } 118 | 119 | func (deps *Deps) FileIndex() fileindex.IFileIndex { 120 | return deps.fileIndex 121 | } 122 | 123 | func (deps *Deps) SetFileIndex(index fileindex.IFileIndex) { 124 | deps.fileIndex = index 125 | } 126 | 127 | func (deps *Deps) DB() db.IDBQuickshare { 128 | return deps.db 129 | } 130 | 131 | func (deps *Deps) SetDB(rdb db.IDBQuickshare) { 132 | deps.db = rdb 133 | } 134 | -------------------------------------------------------------------------------- /src/downloadmgr/mgr.go: -------------------------------------------------------------------------------- 1 | package downloadmgr 2 | 3 | type DownloadMgr struct{} 4 | 5 | func NewDownloadMgr() *DownloadMgr { 6 | return &DownloadMgr{} 7 | } 8 | -------------------------------------------------------------------------------- /src/fs/fs_interface.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | type ReadCloseSeeker interface { 9 | io.Reader 10 | io.ReaderFrom 11 | io.Closer 12 | io.Seeker 13 | } 14 | 15 | type ISimpleFS interface { 16 | Create(path string) error 17 | MkdirAll(path string) error 18 | Remove(path string) error 19 | Rename(oldpath, newpath string) error 20 | ReadAt(path string, b []byte, off int64) (n int, err error) 21 | WriteAt(path string, b []byte, off int64) (n int, err error) 22 | Stat(path string) (os.FileInfo, error) 23 | Close() error 24 | Sync() error 25 | GetFileReader(path string) (ReadCloseSeeker, uint64, error) 26 | CloseReader(id string) error 27 | Root() string 28 | ListDir(path string) ([]os.FileInfo, error) 29 | } 30 | -------------------------------------------------------------------------------- /src/golimiter/limiter.go: -------------------------------------------------------------------------------- 1 | package golimiter 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const expiredCycCount = 3 10 | 11 | type Bucket struct { 12 | refreshedAt time.Time 13 | token int 14 | } 15 | 16 | func NewBucket(token int) *Bucket { 17 | return &Bucket{ 18 | refreshedAt: time.Now(), 19 | token: token, 20 | } 21 | } 22 | 23 | func (b *Bucket) Access(cyc, incr, decr int) bool { 24 | now := time.Now() 25 | 26 | if decr > incr { 27 | return false 28 | } else if b.token >= decr { 29 | b.token -= decr 30 | return true 31 | } 32 | 33 | if b.refreshedAt. 34 | Add(time.Duration(cyc) * time.Millisecond). 35 | After(now) { 36 | return false 37 | } 38 | b.token = incr - decr 39 | b.refreshedAt = now 40 | return true 41 | } 42 | 43 | type Limiter struct { 44 | buckets map[string]*Bucket 45 | cap int 46 | cyc int 47 | cleanBatch int 48 | mtx *sync.RWMutex 49 | } 50 | 51 | func New(cap, cyc int) *Limiter { 52 | if cap <= 0 { 53 | panic("limiter: invalid cap <= 0") 54 | } 55 | if cyc <= 0 { 56 | panic(fmt.Sprintf("limiter: invalid cyc=%d", cyc)) 57 | } 58 | 59 | return &Limiter{ 60 | buckets: make(map[string]*Bucket), 61 | cap: cap, 62 | cyc: cyc, 63 | cleanBatch: 10, 64 | mtx: &sync.RWMutex{}, 65 | } 66 | } 67 | 68 | // func NewWithcleanBatch(cap, cyc, cleanBatch int64, refill int) *Limiter { 69 | // limiter := New(cap, cyc, refill) 70 | // limiter.cleanBatch = cleanBatch 71 | // return limiter 72 | // } 73 | 74 | func (l *Limiter) Access(id string, incr, decr int) bool { 75 | l.mtx.Lock() 76 | defer l.mtx.Unlock() 77 | 78 | b, ok := l.buckets[id] 79 | if !ok { 80 | size := len(l.buckets) 81 | if size > l.cap/2 { 82 | l.clean() 83 | } 84 | 85 | size = len(l.buckets) 86 | if size+1 > l.cap || incr < decr { 87 | return false 88 | } 89 | l.buckets[id] = NewBucket(incr - decr) 90 | return true 91 | } 92 | return b.Access(l.cyc, incr, decr) 93 | } 94 | 95 | func (l *Limiter) clean() { 96 | count := 0 97 | 98 | for key, bucket := range l.buckets { 99 | if bucket.refreshedAt. 100 | Add(time.Duration(l.cyc*expiredCycCount) * time.Millisecond). 101 | Before(time.Now()) { 102 | delete(l.buckets, key) 103 | } 104 | if count++; count >= 10 { 105 | break 106 | } 107 | } 108 | } 109 | 110 | func (l *Limiter) GetCap() int { 111 | l.mtx.RLock() 112 | defer l.mtx.RUnlock() 113 | return l.cap 114 | } 115 | 116 | func (l *Limiter) GetCyc() int { 117 | l.mtx.RLock() 118 | defer l.mtx.RUnlock() 119 | return l.cyc 120 | } 121 | -------------------------------------------------------------------------------- /src/golimiter/limiter_test.go: -------------------------------------------------------------------------------- 1 | package golimiter 2 | 3 | import ( 4 | "fmt" 5 | // "math/rand" 6 | "sync" 7 | "testing" 8 | // "time" 9 | ) 10 | 11 | func TestLimiter(t *testing.T) { 12 | t.Run("access count is limited", func(t *testing.T) { 13 | clientCount := 3 14 | tokenMaxCount := 3 15 | wg := sync.WaitGroup{} 16 | counts := make([]int, clientCount) 17 | 18 | limiter := New(clientCount, 2000) 19 | client := func(id int) { 20 | lid := fmt.Sprint(id) 21 | for i := 0; i < tokenMaxCount*2; i++ { 22 | ok := limiter.Access(lid, tokenMaxCount, 1) 23 | if ok { 24 | counts[id]++ 25 | } 26 | } 27 | 28 | wg.Done() 29 | } 30 | 31 | for i := 0; i < clientCount; i++ { 32 | wg.Add(1) 33 | go client(i) 34 | } 35 | 36 | wg.Wait() 37 | 38 | for id := range counts { 39 | if counts[id] != tokenMaxCount { 40 | t.Fatalf("id(%d): accessed(%d) tokenMaxCount(%d) don't match", id, counts[id], tokenMaxCount) 41 | } 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/handlers/fileshdr/async_handlers.go: -------------------------------------------------------------------------------- 1 | package fileshdr 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | 13 | "github.com/ihexxa/quickshare/src/worker" 14 | ) 15 | 16 | const ( 17 | MsgTypeSha1 = "sha1" 18 | MsgTypeIndexing = "indexing" 19 | ) 20 | 21 | type Sha1Params struct { 22 | FilePath string 23 | UserId uint64 24 | } 25 | 26 | func (h *FileHandlers) genSha1(msg worker.IMsg) error { 27 | taskInputs := &Sha1Params{} 28 | err := json.Unmarshal([]byte(msg.Body()), taskInputs) 29 | if err != nil { 30 | return fmt.Errorf("fail to unmarshal sha1 msg: %w", err) 31 | } 32 | 33 | f, id, err := h.deps.FS().GetFileReader(taskInputs.FilePath) 34 | if err != nil { 35 | return fmt.Errorf("fail to get reader: %s", err) 36 | } 37 | defer func() { 38 | err := h.deps.FS().CloseReader(fmt.Sprint(id)) 39 | if err != nil { 40 | h.deps.Log().Errorf("failed to close file: %s", err) 41 | } 42 | }() 43 | 44 | hasher := sha1.New() 45 | buf := make([]byte, 4096) 46 | _, err = io.CopyBuffer(hasher, f, buf) 47 | if err != nil { 48 | return fmt.Errorf("faile to copy buffer: %w", err) 49 | } 50 | 51 | sha1Sign := fmt.Sprintf("%x", hasher.Sum(nil)) 52 | err = h.deps.FileInfos(). 53 | SetSha1(context.TODO(), taskInputs.FilePath, sha1Sign) // TODO: use source context 54 | if err != nil { 55 | return fmt.Errorf("fail to set sha1: %s", err) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | type IndexingParams struct{} 62 | 63 | func (h *FileHandlers) indexingItems(msg worker.IMsg) error { 64 | err := h.deps.FileIndex().Reset() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | root := "" 70 | queue := []string{root} 71 | var infos []os.FileInfo 72 | for len(queue) > 0 { 73 | pathname := queue[0] 74 | queue = queue[1:] 75 | infos, err = h.deps.FS().ListDir(pathname) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | for _, fileInfo := range infos { 81 | childPath := path.Join(pathname, fileInfo.Name()) 82 | if fileInfo.IsDir() { 83 | queue = append(queue, childPath) 84 | } else { 85 | err = h.deps.FileIndex().AddPath(childPath) 86 | if err != nil { 87 | return err 88 | } 89 | } 90 | } 91 | } 92 | 93 | h.deps.Log().Info("reindexing done") 94 | return nil 95 | } 96 | 97 | const ( 98 | MsgTypeResetUsedSpace = "reset-used-space" 99 | ) 100 | 101 | type UsedSpaceParams struct { 102 | UserID uint64 103 | UserHomePath string 104 | } 105 | 106 | func (h *FileHandlers) resetUsedSpace(msg worker.IMsg) error { 107 | params := &UsedSpaceParams{} 108 | err := json.Unmarshal([]byte(msg.Body()), params) 109 | if err != nil { 110 | return fmt.Errorf("fail to unmarshal sha1 msg: %w", err) 111 | } 112 | 113 | usedSpace := int64(0) 114 | dirQueue := []string{params.UserHomePath} 115 | for len(dirQueue) > 0 { 116 | dirPath := dirQueue[0] 117 | dirQueue = dirQueue[1:] 118 | 119 | infos, err := h.deps.FS().ListDir(dirPath) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | for _, info := range infos { 125 | if info.IsDir() { 126 | dirQueue = append(dirQueue, filepath.Join(dirPath, info.Name())) 127 | } else { 128 | usedSpace += info.Size() 129 | } 130 | } 131 | } 132 | 133 | return h.deps.Users().ResetUsed(context.TODO(), params.UserID, usedSpace) // TODO: use source context 134 | } 135 | -------------------------------------------------------------------------------- /src/handlers/fileshdr/init.go: -------------------------------------------------------------------------------- 1 | package fileshdr 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/ihexxa/quickshare/src/db" 9 | q "github.com/ihexxa/quickshare/src/handlers" 10 | "golang.org/x/crypto/bcrypt" 11 | ) 12 | 13 | func (h *FileHandlers) Init(ctx context.Context, adminName string) (string, error) { 14 | var err error 15 | 16 | fsPath := q.FsRootPath(adminName, "") 17 | if err = h.deps.FS().MkdirAll(fsPath); err != nil { 18 | return "", err 19 | } 20 | uploadFolder := q.UploadFolder(adminName) 21 | if err = h.deps.FS().MkdirAll(uploadFolder); err != nil { 22 | return "", err 23 | } 24 | 25 | usersInterface, ok := h.cfg.Slice("Users.PredefinedUsers") 26 | spaceLimit := int64(h.cfg.IntOr("Users.SpaceLimit", 100*1024*1024)) 27 | uploadSpeedLimit := h.cfg.IntOr("Users.UploadSpeedLimit", 100*1024) 28 | downloadSpeedLimit := h.cfg.IntOr("Users.DownloadSpeedLimit", 100*1024) 29 | if downloadSpeedLimit < q.DownloadChunkSize { 30 | return "", fmt.Errorf("download speed limit can not be lower than chunk size: %d", q.DownloadChunkSize) 31 | } 32 | if ok { 33 | userCfgs, ok := usersInterface.([]*db.UserCfg) 34 | if !ok { 35 | return "", fmt.Errorf("predefined user is invalid: %s", err) 36 | } 37 | for _, userCfg := range userCfgs { 38 | _, err := h.deps.Users().GetUserByName(ctx, userCfg.Name) 39 | if err != nil { 40 | if errors.Is(err, db.ErrUserNotFound) { 41 | // no op, need initing 42 | } else { 43 | return "", err 44 | } 45 | } else { 46 | h.deps.Log().Warn("warning: users exists, skip initing(%s)", userCfg.Name) 47 | continue 48 | } 49 | 50 | // TODO: following operations must be atomic 51 | // TODO: check if the folders already exists 52 | fsRootFolder := q.FsRootPath(userCfg.Name, "") 53 | if err = h.deps.FS().MkdirAll(fsRootFolder); err != nil { 54 | return "", err 55 | } 56 | uploadFolder := q.UploadFolder(userCfg.Name) 57 | if err = h.deps.FS().MkdirAll(uploadFolder); err != nil { 58 | return "", err 59 | } 60 | 61 | pwdHash, err := bcrypt.GenerateFromPassword([]byte(userCfg.Pwd), 10) 62 | if err != nil { 63 | return "", err 64 | } 65 | 66 | preferences := db.DefaultPreferences 67 | user := &db.User{ 68 | ID: h.deps.ID().Gen(), 69 | Name: userCfg.Name, 70 | Pwd: string(pwdHash), 71 | Role: userCfg.Role, 72 | Quota: &db.Quota{ 73 | SpaceLimit: spaceLimit, 74 | UploadSpeedLimit: uploadSpeedLimit, 75 | DownloadSpeedLimit: downloadSpeedLimit, 76 | }, 77 | Preferences: &preferences, 78 | } 79 | 80 | err = h.deps.Users().AddUser(ctx, user) 81 | if err != nil { 82 | h.deps.Log().Warn("warning: failed to add user(%s): %s", user, err) 83 | return "", err 84 | } 85 | h.deps.Log().Infof("user(%s) is added", user.Name) 86 | } 87 | } 88 | return "", nil 89 | } 90 | -------------------------------------------------------------------------------- /src/handlers/multiusers/async_handlers.go: -------------------------------------------------------------------------------- 1 | package multiusers 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "path/filepath" 8 | 9 | "github.com/ihexxa/quickshare/src/worker" 10 | ) 11 | 12 | const ( 13 | MsgTypeResetUsedSpace = "reset-used-space" 14 | ) 15 | 16 | type UsedSpaceParams struct { 17 | UserID uint64 18 | UserHomePath string 19 | } 20 | 21 | func (h *MultiUsersSvc) resetUsedSpace(msg worker.IMsg) error { 22 | params := &UsedSpaceParams{} 23 | err := json.Unmarshal([]byte(msg.Body()), params) 24 | if err != nil { 25 | return fmt.Errorf("fail to unmarshal sha1 msg: %w", err) 26 | } 27 | 28 | usedSpace := int64(0) 29 | dirQueue := []string{params.UserHomePath} 30 | for len(dirQueue) > 0 { 31 | dirPath := dirQueue[0] 32 | dirQueue = dirQueue[1:] 33 | 34 | infos, err := h.deps.FS().ListDir(dirPath) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | for _, info := range infos { 40 | if info.IsDir() { 41 | dirQueue = append(dirQueue, filepath.Join(dirPath, info.Name())) 42 | } else { 43 | usedSpace += info.Size() 44 | } 45 | } 46 | } 47 | 48 | return h.deps.Users().ResetUsed(context.TODO(), params.UserID, usedSpace) // TODO: use source context 49 | } 50 | -------------------------------------------------------------------------------- /src/handlers/multiusers/captcha.go: -------------------------------------------------------------------------------- 1 | package multiusers 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | 7 | "github.com/dchest/captcha" 8 | "github.com/gin-gonic/gin" 9 | 10 | q "github.com/ihexxa/quickshare/src/handlers" 11 | ) 12 | 13 | type GetCaptchaIDResp struct { 14 | CaptchaID string `json:"id"` 15 | } 16 | 17 | func (h *MultiUsersSvc) GetCaptchaID(c *gin.Context) { 18 | captchaID := captcha.New() 19 | c.JSON(200, &GetCaptchaIDResp{CaptchaID: captchaID}) 20 | } 21 | 22 | // path: /captchas/imgs?id=xxx 23 | func (h *MultiUsersSvc) GetCaptchaImg(c *gin.Context) { 24 | captchaID := c.Query(q.CaptchaIDParam) 25 | if captchaID == "" { 26 | c.JSON(q.ErrResp(c, 400, errors.New("empty captcha ID"))) 27 | return 28 | } 29 | 30 | capWidth := h.cfg.IntOr("Users.CaptchaWidth", 256) 31 | capHeight := h.cfg.IntOr("Users.CaptchaHeight", 64) 32 | 33 | // TODO: improve performance 34 | buf := new(bytes.Buffer) 35 | err := captcha.WriteImage(buf, captchaID, capWidth, capHeight) 36 | if err != nil { 37 | c.JSON(q.ErrResp(c, 500, err)) 38 | return 39 | } 40 | 41 | c.Data(200, "image/png", buf.Bytes()) 42 | } 43 | -------------------------------------------------------------------------------- /src/handlers/multiusers/middlewares.go: -------------------------------------------------------------------------------- 1 | package multiusers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | "github.com/ihexxa/quickshare/src/db" 14 | q "github.com/ihexxa/quickshare/src/handlers" 15 | ) 16 | 17 | var ErrExpired = errors.New("token is expired") 18 | 19 | func apiRuleCname(role, method, path string) string { 20 | return fmt.Sprintf("%s-%s-%s", role, method, path) 21 | } 22 | 23 | func (h *MultiUsersSvc) AuthN() gin.HandlerFunc { 24 | return func(c *gin.Context) { 25 | enableAuth := h.cfg.GrabBool("Users.EnableAuth") 26 | claims := map[string]string{ 27 | q.UserIDParam: "", 28 | q.UserParam: "", 29 | q.RoleParam: db.VisitorRole, 30 | q.ExpireParam: "", 31 | } 32 | 33 | if enableAuth { 34 | token, err := c.Cookie(q.TokenCookie) 35 | if err != nil { 36 | if err != http.ErrNoCookie { 37 | c.AbortWithStatusJSON(q.ErrResp(c, 401, err)) 38 | return 39 | } 40 | // set default values if no cookie is found 41 | } else if token != "" { 42 | claims, err = h.deps.Token().FromToken(token, claims) 43 | if err != nil { 44 | c.AbortWithStatusJSON(q.ErrResp(c, 401, err)) 45 | return 46 | } 47 | 48 | now := time.Now().Unix() 49 | expire, err := strconv.ParseInt(claims[q.ExpireParam], 10, 64) 50 | if err != nil { 51 | c.AbortWithStatusJSON(q.ErrResp(c, 401, err)) 52 | return 53 | } else if expire <= now { 54 | c.AbortWithStatusJSON(q.ErrResp(c, 401, ErrExpired)) 55 | return 56 | } 57 | } 58 | // set default values if token is empty 59 | } else { 60 | claims[q.UserIDParam] = "0" 61 | claims[q.UserParam] = "admin" 62 | claims[q.RoleParam] = db.AdminRole 63 | claims[q.ExpireParam] = "" 64 | } 65 | 66 | for key, val := range claims { 67 | c.Set(key, val) 68 | } 69 | c.Next() 70 | } 71 | } 72 | 73 | func (h *MultiUsersSvc) APIAccessControl() gin.HandlerFunc { 74 | return func(c *gin.Context) { 75 | role := c.MustGet(q.RoleParam).(string) 76 | method := c.Request.Method 77 | accessPath := c.Request.URL.Path 78 | 79 | if role == db.BannedRole { 80 | c.AbortWithStatusJSON(q.ErrResp(c, 403, q.ErrAccessDenied)) 81 | } 82 | 83 | // v2 ac control 84 | matches := h.routeRules.GetAllPrefixMatches(accessPath) 85 | key := fmt.Sprintf("%s:%s", role, method) 86 | matched := false 87 | for _, matchedRules := range matches { 88 | matchedRuleMap := matchedRules.(map[string]bool) 89 | if matchedRuleMap[key] { 90 | matched = true 91 | break 92 | } 93 | } 94 | 95 | // TODO: listDir and download are exceptions: for sharing 96 | if accessPath == "/v2/my/fs/dirs" { 97 | matched = true 98 | } 99 | 100 | if matched { 101 | c.Next() 102 | return 103 | } 104 | 105 | if h.apiACRules[apiRuleCname(role, method, accessPath)] { 106 | c.Next() 107 | return 108 | } else if accessPath == "/" || // TODO: temporarily allow accessing static resources 109 | accessPath == "/favicon.ico" || 110 | strings.HasPrefix(accessPath, "/css") || 111 | strings.HasPrefix(accessPath, "/font") || 112 | strings.HasPrefix(accessPath, "/img") || 113 | strings.HasPrefix(accessPath, "/js") { 114 | c.Next() 115 | return 116 | } 117 | c.AbortWithStatusJSON(q.ErrResp(c, 403, q.ErrAccessDenied)) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/handlers/settings/handlers.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/ihexxa/gocfg" 8 | 9 | "github.com/ihexxa/quickshare/src/db" 10 | "github.com/ihexxa/quickshare/src/depidx" 11 | q "github.com/ihexxa/quickshare/src/handlers" 12 | ) 13 | 14 | type SettingsSvc struct { 15 | cfg gocfg.ICfg 16 | deps *depidx.Deps 17 | } 18 | 19 | func NewSettingsSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*SettingsSvc, error) { 20 | return &SettingsSvc{ 21 | cfg: cfg, 22 | deps: deps, 23 | }, nil 24 | } 25 | 26 | func (h *SettingsSvc) Health(c *gin.Context) { 27 | // TODO: currently it checks nothing 28 | c.JSON(q.Resp(200)) 29 | } 30 | 31 | type ClientCfgMsg struct { 32 | ClientCfg *db.ClientConfig `json:"clientCfg"` 33 | CaptchaEnabled bool `json:"captchaEnabled"` 34 | } 35 | 36 | func (h *SettingsSvc) GetClientCfg(c *gin.Context) { 37 | // TODO: add cache 38 | siteCfg, err := h.deps.SiteStore().GetCfg(c) 39 | if err != nil { 40 | c.JSON(q.ErrResp(c, 500, err)) 41 | return 42 | } 43 | 44 | c.JSON(200, &ClientCfgMsg{ 45 | ClientCfg: siteCfg.ClientCfg, 46 | CaptchaEnabled: h.cfg.BoolOr("Users.CaptchaEnabled", true), 47 | }) 48 | } 49 | 50 | func (h *SettingsSvc) SetClientCfg(c *gin.Context) { 51 | var err error 52 | req := &ClientCfgMsg{} 53 | if err = c.ShouldBindJSON(&req); err != nil { 54 | c.JSON(q.ErrResp(c, 400, err)) 55 | return 56 | } 57 | 58 | // TODO: captchaEnabled is not persisted in db 59 | clientCfg := req.ClientCfg 60 | if err = validateClientCfg(clientCfg); err != nil { 61 | c.JSON(q.ErrResp(c, 400, err)) 62 | return 63 | } 64 | 65 | // update config 66 | // TODO: refine the model 67 | h.cfg.SetString("Server.Dynamic.ClientCfg.SiteName", req.ClientCfg.SiteName) 68 | h.cfg.SetString("Server.Dynamic.ClientCfg.SiteDesc", req.ClientCfg.SiteDesc) 69 | h.cfg.SetString("Server.Dynamic.ClientCfg.Bg.Url", req.ClientCfg.Bg.Url) 70 | h.cfg.SetString("Server.Dynamic.ClientCfg.Bg.Repeat", req.ClientCfg.Bg.Repeat) 71 | h.cfg.SetString("Server.Dynamic.ClientCfg.Bg.Position", req.ClientCfg.Bg.Position) 72 | h.cfg.SetString("Server.Dynamic.ClientCfg.Bg.Align", req.ClientCfg.Bg.Align) 73 | h.cfg.SetString("Server.Dynamic.ClientCfg.Bg.BgColor", req.ClientCfg.Bg.BgColor) 74 | h.cfg.SetBool("Server.Dynamic.ClientCfg.AllowSetBg", req.ClientCfg.AllowSetBg) 75 | h.cfg.SetBool("Server.Dynamic.ClientCfg.AutoTheme", req.ClientCfg.AutoTheme) 76 | 77 | err = h.deps.SiteStore().SetClientCfg(c, clientCfg) 78 | if err != nil { 79 | c.JSON(q.ErrResp(c, 500, err)) 80 | return 81 | } 82 | c.JSON(q.Resp(200)) 83 | } 84 | 85 | func validateClientCfg(cfg *db.ClientConfig) error { 86 | if len(cfg.SiteName) == 0 || len(cfg.SiteName) >= 12 { 87 | return errors.New("site name is too short or too long") 88 | } else if len(cfg.SiteDesc) >= 64 { 89 | return errors.New("site description is too short or too long") 90 | } 91 | return nil 92 | } 93 | 94 | type ClientErrorReport struct { 95 | Report string `json:"report"` 96 | Version string `json:"version"` 97 | } 98 | 99 | type ClientErrorReports struct { 100 | Reports []*ClientErrorReport `json:"reports"` 101 | } 102 | 103 | func (h *SettingsSvc) ReportErrors(c *gin.Context) { 104 | var err error 105 | req := &ClientErrorReports{} 106 | if err = c.ShouldBindJSON(&req); err != nil { 107 | c.JSON(q.ErrResp(c, 400, err)) 108 | return 109 | } 110 | 111 | for _, report := range req.Reports { 112 | h.deps.Log().Errorf("version:%s,error:%s", report.Version, report.Report) 113 | } 114 | c.JSON(q.Resp(200)) 115 | } 116 | 117 | type WorkerQueueLenResp struct { 118 | QueueLen int `json:"queueLen"` 119 | } 120 | 121 | func (h *SettingsSvc) WorkerQueueLen(c *gin.Context) { 122 | c.JSON(200, &WorkerQueueLenResp{ 123 | QueueLen: h.deps.Workers().QueueLen(), 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /src/idgen/idgen_interface.go: -------------------------------------------------------------------------------- 1 | package idgen 2 | 3 | type IIDGen interface { 4 | Gen() uint64 5 | } 6 | -------------------------------------------------------------------------------- /src/idgen/simpleidgen/simple_id_gen.go: -------------------------------------------------------------------------------- 1 | package simpleidgen 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | var lastID = uint64(0) 9 | var mux = &sync.Mutex{} 10 | 11 | type SimpleIDGen struct{} 12 | 13 | func New() *SimpleIDGen { 14 | return &SimpleIDGen{} 15 | } 16 | 17 | func (id *SimpleIDGen) Gen() uint64 { 18 | mux.Lock() 19 | defer mux.Unlock() 20 | newID := uint64(time.Now().UnixNano()) 21 | if newID != lastID { 22 | lastID = newID 23 | return lastID 24 | } 25 | lastID = newID + 1 26 | return lastID 27 | } 28 | -------------------------------------------------------------------------------- /src/iolimiter/iolimiter.go: -------------------------------------------------------------------------------- 1 | package iolimiter 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/ihexxa/quickshare/src/db" 9 | "github.com/ihexxa/quickshare/src/golimiter" 10 | ) 11 | 12 | const cacheSizeLimit = 1024 13 | 14 | type ILimiter interface { 15 | CanWrite(userID uint64, chunkSize int) (bool, error) 16 | CanRead(userID uint64, chunkSize int) (bool, error) 17 | } 18 | 19 | type IOLimiter struct { 20 | mtx *sync.Mutex 21 | UploadLimiter *golimiter.Limiter 22 | DownloadLimiter *golimiter.Limiter 23 | users db.IUserDB 24 | quotaCache map[uint64]*db.Quota 25 | } 26 | 27 | func NewIOLimiter(cap, cyc int, users db.IUserDB) *IOLimiter { 28 | return &IOLimiter{ 29 | mtx: &sync.Mutex{}, 30 | UploadLimiter: golimiter.New(cap, cyc), 31 | DownloadLimiter: golimiter.New(cap, cyc), 32 | users: users, 33 | quotaCache: map[uint64]*db.Quota{}, 34 | } 35 | } 36 | 37 | func (lm *IOLimiter) CanWrite(id uint64, chunkSize int) (bool, error) { 38 | lm.mtx.Lock() 39 | defer lm.mtx.Unlock() 40 | 41 | quota, ok := lm.quotaCache[id] 42 | if !ok { 43 | user, err := lm.users.GetUser(context.TODO(), id) // TODO: add context 44 | if err != nil { 45 | return false, err 46 | } 47 | quota = user.Quota 48 | lm.quotaCache[id] = quota 49 | } 50 | if len(lm.quotaCache) > cacheSizeLimit { 51 | lm.clean() 52 | } 53 | 54 | return lm.UploadLimiter.Access( 55 | fmt.Sprint(id), 56 | quota.UploadSpeedLimit, 57 | chunkSize, 58 | ), nil 59 | } 60 | 61 | func (lm *IOLimiter) CanRead(id uint64, chunkSize int) (bool, error) { 62 | lm.mtx.Lock() 63 | defer lm.mtx.Unlock() 64 | 65 | quota, ok := lm.quotaCache[id] 66 | if !ok { 67 | user, err := lm.users.GetUser(context.TODO(), id) // TODO: add context 68 | if err != nil { 69 | return false, err 70 | } 71 | quota = user.Quota 72 | lm.quotaCache[id] = quota 73 | } 74 | if len(lm.quotaCache) > cacheSizeLimit { 75 | lm.clean() 76 | } 77 | 78 | return lm.DownloadLimiter.Access( 79 | fmt.Sprint(id), 80 | quota.DownloadSpeedLimit, 81 | chunkSize, 82 | ), nil 83 | } 84 | 85 | func (lm *IOLimiter) clean() { 86 | count := 0 87 | for key := range lm.quotaCache { 88 | delete(lm.quotaCache, key) 89 | if count++; count > 5 { 90 | break 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/kvstore/boltdbpvd/interface.go: -------------------------------------------------------------------------------- 1 | package boltdbpvd 2 | 3 | import "github.com/boltdb/bolt" 4 | 5 | type BoltProvider interface { 6 | Bolt() *bolt.DB 7 | } 8 | -------------------------------------------------------------------------------- /src/kvstore/kvstore_interface.go: -------------------------------------------------------------------------------- 1 | package kvstore 2 | 3 | import "errors" 4 | 5 | var ErrLocked = errors.New("already locked") 6 | var ErrNoLock = errors.New("no lock to unlock") 7 | 8 | // Deprecated: no longer supported 9 | type IKVStore interface { 10 | AddNamespace(nsName string) error 11 | DelNamespace(nsName string) error 12 | HasNamespace(nsName string) bool 13 | GetBool(key string) (bool, bool) 14 | GetBoolIn(ns, key string) (bool, bool) 15 | SetBool(key string, val bool) error 16 | SetBoolIn(ns, key string, val bool) error 17 | DelBool(key string) error 18 | DelBoolIn(ns, key string) error 19 | ListBools() (map[string]bool, error) 20 | ListBoolsIn(ns string) (map[string]bool, error) 21 | ListBoolsByPrefixIn(prefix, ns string) (map[string]bool, error) 22 | GetInt(key string) (int, bool) 23 | SetInt(key string, val int) error 24 | DelInt(key string) error 25 | GetInt64(key string) (int64, bool) 26 | SetInt64(key string, val int64) error 27 | GetInt64In(ns, key string) (int64, bool) 28 | SetInt64In(ns, key string, val int64) error 29 | ListInt64sIn(ns string) (map[string]int64, error) 30 | DelInt64(key string) error 31 | DelInt64In(ns, key string) error 32 | GetFloat(key string) (float64, bool) 33 | SetFloat(key string, val float64) error 34 | DelFloat(key string) error 35 | GetString(key string) (string, bool) 36 | SetString(key, val string) error 37 | DelString(key string) error 38 | DelStringIn(ns, key string) error 39 | GetStringIn(ns, key string) (string, bool) 40 | SetStringIn(ns, key, val string) error 41 | ListStringsIn(ns string) (map[string]string, error) 42 | ListStringsByPrefixIn(prefix, ns string) (map[string]string, error) 43 | TryLock(key string) error 44 | Unlock(key string) error 45 | } 46 | -------------------------------------------------------------------------------- /src/search/fileindex/fsearch.go: -------------------------------------------------------------------------------- 1 | package fileindex 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/ihexxa/fsearch" 11 | "github.com/ihexxa/quickshare/src/fs" 12 | ) 13 | 14 | type IFileIndex interface { 15 | Search(keyword string) ([]string, error) 16 | AddPath(pathname string) error 17 | DelPath(pathname string) error 18 | RenamePath(pathname, newName string) error 19 | MovePath(pathname, dstParentPath string) error 20 | WriteTo(pathname string) error 21 | ReadFrom(pathname string) error 22 | Reset() error 23 | String() string 24 | } 25 | 26 | type FileTreeIndex struct { 27 | fs fs.ISimpleFS 28 | index *fsearch.FSearch 29 | pathSeparator string 30 | maxResultSize int 31 | } 32 | 33 | func NewFileTreeIndex(fs fs.ISimpleFS, pathSeparator string, maxResultSize int) *FileTreeIndex { 34 | return &FileTreeIndex{ 35 | fs: fs, 36 | index: fsearch.New(pathSeparator, maxResultSize), 37 | pathSeparator: pathSeparator, 38 | maxResultSize: maxResultSize, 39 | } 40 | } 41 | 42 | func (idx *FileTreeIndex) Reset() error { 43 | idx.index = fsearch.New(idx.pathSeparator, idx.maxResultSize) 44 | return nil 45 | } 46 | 47 | func (idx *FileTreeIndex) Search(keyword string) ([]string, error) { 48 | return idx.index.Search(keyword) 49 | } 50 | 51 | func (idx *FileTreeIndex) AddPath(pathname string) error { 52 | return idx.index.AddPath(pathname) 53 | } 54 | 55 | func (idx *FileTreeIndex) DelPath(pathname string) error { 56 | return idx.index.DelPath(pathname) 57 | } 58 | 59 | func (idx *FileTreeIndex) RenamePath(pathname, newName string) error { 60 | return idx.index.RenamePath(pathname, newName) 61 | } 62 | 63 | func (idx *FileTreeIndex) MovePath(pathname, dstParentPath string) error { 64 | return idx.index.MovePath(pathname, dstParentPath) 65 | } 66 | 67 | func (idx *FileTreeIndex) WriteTo(pathname string) error { 68 | rowsChan := idx.index.Marshal() 69 | err := idx.fs.Remove(pathname) 70 | if err != nil { 71 | return err 72 | } 73 | err = idx.fs.Create(pathname) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | var row string 79 | var ok bool 80 | var wrote int 81 | var offset int64 82 | batch := []string{} 83 | for { 84 | row, ok = <-rowsChan 85 | if ok { 86 | batch = append(batch, row+"\n") 87 | } 88 | if !ok || len(batch) > 1024 { 89 | wrote, err = idx.fs.WriteAt(pathname, []byte(strings.Join(batch, "")), offset) 90 | if err != nil { 91 | return err 92 | } 93 | offset += int64(wrote) 94 | batch = batch[:0] 95 | } 96 | if !ok { 97 | break 98 | } 99 | } 100 | 101 | return idx.index.Error() 102 | } 103 | 104 | func (idx *FileTreeIndex) ReadFrom(pathname string) error { 105 | f, readerId, err := idx.fs.GetFileReader(pathname) 106 | if err != nil { 107 | return err 108 | } 109 | defer idx.fs.CloseReader(fmt.Sprint(readerId)) 110 | 111 | var row string 112 | rowSeparator := byte('\n') 113 | reader := bufio.NewReader(f) 114 | rowsChan := make(chan string, 1024) 115 | 116 | var workerErr error 117 | readWorker := func() { 118 | defer close(rowsChan) 119 | for { 120 | row, err = reader.ReadString(rowSeparator) 121 | if err != nil { 122 | if errors.Is(err, io.EOF) { 123 | if row != "" { 124 | rowsChan <- row 125 | } 126 | break 127 | } else { 128 | workerErr = err 129 | break 130 | } 131 | } 132 | if row != "" { 133 | rowsChan <- row 134 | } 135 | } 136 | } 137 | go readWorker() 138 | 139 | idx.index.Unmarshal(rowsChan) 140 | if workerErr != nil { 141 | return err 142 | } 143 | return idx.index.Error() 144 | } 145 | 146 | func (idx *FileTreeIndex) String() string { 147 | return idx.index.String() 148 | } 149 | -------------------------------------------------------------------------------- /src/search/fileindex/fsearch_test.go: -------------------------------------------------------------------------------- 1 | package fileindex 2 | 3 | import ( 4 | "math/rand" 5 | "os" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/ihexxa/quickshare/src/fs/local" 11 | "github.com/ihexxa/quickshare/src/idgen/simpleidgen" 12 | "github.com/ihexxa/randstr" 13 | ) 14 | 15 | func TestFileSearch(t *testing.T) { 16 | dirPath := "tmp" 17 | err := os.MkdirAll(dirPath, 0700) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | defer os.RemoveAll(dirPath) 22 | 23 | makePaths := func(maxPathLen, count int) map[string]bool { 24 | rand.Seed(time.Now().UnixNano()) 25 | randStr := randstr.New([]string{}) 26 | 27 | paths := map[string]bool{} 28 | for i := 0; i < count; i++ { 29 | pathLen := rand.Intn(maxPathLen) + 1 30 | pathParts := []string{} 31 | for j := 0; j < pathLen; j++ { 32 | pathParts = append(pathParts, randStr.Alnums()) 33 | } 34 | paths[strings.Join(pathParts, "/")] = true 35 | } 36 | 37 | return paths 38 | } 39 | 40 | ider := simpleidgen.New() 41 | fs := local.NewLocalFS(dirPath, 0660, 1024, 60, 60, ider) 42 | fileIndex := NewFileTreeIndex(fs, "/", 0) 43 | 44 | paths := makePaths(8, 256) 45 | for pathname := range paths { 46 | err := fileIndex.AddPath(pathname) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | } 51 | 52 | indexPath := "/fileindex" 53 | err = fileIndex.WriteTo(indexPath) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | fileIndex2 := NewFileTreeIndex(fs, "/", 0) 59 | err = fileIndex2.ReadFrom(indexPath) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/ihexxa/gocfg" 16 | 17 | "github.com/ihexxa/quickshare/src/depidx" 18 | "github.com/ihexxa/quickshare/src/fs" 19 | ) 20 | 21 | type Server struct { 22 | server *http.Server 23 | cfg gocfg.ICfg 24 | deps *depidx.Deps 25 | signalChan chan os.Signal 26 | } 27 | 28 | func NewServer(cfg gocfg.ICfg) (*Server, error) { 29 | if !cfg.BoolOr("Server.Debug", false) { 30 | gin.SetMode(gin.ReleaseMode) 31 | } 32 | 33 | initer := NewIniter(cfg) 34 | deps := initer.InitDeps() 35 | router, err := initer.InitHandlers(deps) 36 | if err != nil { 37 | return nil, fmt.Errorf("init handlers error: %w", err) 38 | } 39 | 40 | port := cfg.GrabInt("Server.Port") 41 | portStr, ok := cfg.String("ENV.PORT") 42 | if ok && portStr != "" { 43 | port, err = strconv.Atoi(portStr) 44 | if err != nil { 45 | deps.Log().Fatalf("invalid port: %s", portStr) 46 | } 47 | cfg.SetInt("Server.Port", port) 48 | } 49 | 50 | srv := &http.Server{ 51 | Addr: fmt.Sprintf("%s:%d", cfg.GrabString("Server.Host"), port), 52 | Handler: router, 53 | ReadTimeout: time.Duration(cfg.GrabInt("Server.ReadTimeout")) * time.Millisecond, 54 | WriteTimeout: time.Duration(cfg.GrabInt("Server.WriteTimeout")) * time.Millisecond, 55 | MaxHeaderBytes: cfg.GrabInt("Server.MaxHeaderBytes"), 56 | } 57 | 58 | return &Server{ 59 | server: srv, 60 | deps: deps, 61 | cfg: cfg, 62 | }, nil 63 | } 64 | 65 | func (s *Server) Start() error { 66 | s.signalChan = make(chan os.Signal, 4) 67 | signal.Notify(s.signalChan, syscall.SIGINT, syscall.SIGTERM) 68 | go func() { 69 | sig := <-s.signalChan 70 | if sig != nil { 71 | s.deps.Log().Infow( 72 | fmt.Sprintf("received signal %s: shutting down", sig.String()), 73 | ) 74 | } 75 | s.Shutdown() 76 | }() 77 | 78 | s.deps.Log().Infow( 79 | "quickshare is starting", 80 | "hostname:port", 81 | fmt.Sprintf( 82 | "%s:%d", 83 | s.cfg.GrabString("Server.Host"), 84 | s.cfg.GrabInt("Server.Port"), 85 | ), 86 | ) 87 | 88 | err := s.server.ListenAndServe() 89 | if err != http.ErrServerClosed { 90 | return fmt.Errorf("listen error: %w", err) 91 | } 92 | return nil 93 | } 94 | 95 | func (s *Server) Shutdown() error { 96 | // TODO: add timeout 97 | err := s.deps.FileIndex().WriteTo(fileIndexPath) 98 | if err != nil { 99 | s.deps.Log().Errorf("failed to persist file index: %s", err) 100 | } 101 | s.deps.Workers().Stop() 102 | err = s.deps.FS().Close() 103 | if err != nil { 104 | s.deps.Log().Errorf("failed to close file system: %s", err) 105 | } 106 | err = s.deps.DB().Close() 107 | if err != nil { 108 | s.deps.Log().Errorf("failed to close database: %s", err) 109 | } 110 | err = s.server.Shutdown(context.Background()) 111 | if err != nil { 112 | s.deps.Log().Errorf("failed to shutdown server: %s", err) 113 | } 114 | 115 | s.deps.Log().Sync() 116 | return nil 117 | } 118 | 119 | func (s *Server) depsFS() fs.ISimpleFS { 120 | return s.deps.FS() 121 | } 122 | -------------------------------------------------------------------------------- /src/server/server_bench_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/ihexxa/quickshare/src/client" 9 | q "github.com/ihexxa/quickshare/src/handlers" 10 | ) 11 | 12 | func BenchmarkUploadAndDownload(b *testing.B) { 13 | addr := "http://127.0.0.1:8686" 14 | rootPath := "tmpTestData" 15 | config := `{ 16 | "users": { 17 | "enableAuth": true, 18 | "minUserNameLen": 2, 19 | "minPwdLen": 4, 20 | "captchaEnabled": false, 21 | "uploadSpeedLimit": 409600, 22 | "downloadSpeedLimit": 409600, 23 | "spaceLimit": 1024, 24 | "limiterCapacity": 1000, 25 | "limiterCyc": 1000 26 | }, 27 | "server": { 28 | "debug": true, 29 | "host": "127.0.0.1" 30 | }, 31 | "fs": { 32 | "root": "tmpTestData", 33 | "opensLimit": 1024 34 | }, 35 | "db": { 36 | "dbPath": "tmpTestData/quickshare" 37 | } 38 | }` 39 | 40 | adminName := "qs" 41 | adminPwd := "quicksh@re" 42 | setUpEnv(b, rootPath, adminName, adminPwd) 43 | defer os.RemoveAll(rootPath) 44 | 45 | srv := startTestServer(config) 46 | defer srv.Shutdown() 47 | if !isServerReady(addr) { 48 | b.Fatal("fail to start server") 49 | } 50 | 51 | adminUsersCli := client.NewUsersClient(addr) 52 | resp, _, errs := adminUsersCli.Login(adminName, adminPwd) 53 | if len(errs) > 0 { 54 | b.Fatal(errs) 55 | } else if resp.StatusCode != 200 { 56 | b.Fatal(resp.StatusCode) 57 | } 58 | adminToken := client.GetCookie(resp.Cookies(), q.TokenCookie) 59 | 60 | userCount := 5 61 | userPwd := "1234" 62 | users := addUsers(b, addr, userPwd, userCount, adminToken) 63 | filesCount := 30 64 | rounds := 1 65 | 66 | clients := []*MockClient{} 67 | for i := 0; i < b.N; i++ { 68 | for j := 0; j < rounds; j++ { 69 | var wg sync.WaitGroup 70 | for userName := range users { 71 | client := &MockClient{errs: []error{}} 72 | clients = append(clients, client) 73 | wg.Add(1) 74 | go client.uploadAndDownload(b, addr, userName, userPwd, filesCount, &wg) 75 | } 76 | 77 | wg.Wait() 78 | 79 | errs := []error{} 80 | for _, client := range clients { 81 | if len(client.errs) > 0 { 82 | errs = append(errs, client.errs...) 83 | } 84 | } 85 | if len(errs) > 0 { 86 | b.Fatal(joinErrs(errs)) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/server/server_concurrency_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/ihexxa/quickshare/src/client" 9 | q "github.com/ihexxa/quickshare/src/handlers" 10 | ) 11 | 12 | func TestConcurrency(t *testing.T) { 13 | addr := "http://127.0.0.1:8686" 14 | rootPath := "tmpTestData" 15 | config := `{ 16 | "users": { 17 | "enableAuth": true, 18 | "minUserNameLen": 2, 19 | "minPwdLen": 4, 20 | "captchaEnabled": false, 21 | "uploadSpeedLimit": 409600, 22 | "downloadSpeedLimit": 409600, 23 | "spaceLimit": 1024, 24 | "limiterCapacity": 1000, 25 | "limiterCyc": 1000 26 | }, 27 | "server": { 28 | "debug": true, 29 | "host": "127.0.0.1" 30 | }, 31 | "fs": { 32 | "root": "tmpTestData" 33 | }, 34 | "db": { 35 | "dbPath": "tmpTestData/quickshare" 36 | } 37 | }` 38 | 39 | adminName := "qs" 40 | adminPwd := "quicksh@re" 41 | setUpEnv(t, rootPath, adminName, adminPwd) 42 | defer os.RemoveAll(rootPath) 43 | 44 | srv := startTestServer(config) 45 | defer srv.Shutdown() 46 | if !isServerReady(addr) { 47 | t.Fatal("fail to start server") 48 | } 49 | 50 | adminUsersCli := client.NewUsersClient(addr) 51 | resp, _, errs := adminUsersCli.Login(adminName, adminPwd) 52 | if len(errs) > 0 { 53 | t.Fatal(errs) 54 | } else if resp.StatusCode != 200 { 55 | t.Fatal(resp.StatusCode) 56 | } 57 | adminToken := client.GetCookie(resp.Cookies(), q.TokenCookie) 58 | 59 | userCount := 2 60 | userPwd := "1234" 61 | users := addUsers(t, addr, userPwd, userCount, adminToken) 62 | filesCount := 5 63 | rounds := 2 64 | 65 | t.Run("Upload and download concurrently", func(t *testing.T) { 66 | for i := 0; i < rounds; i++ { 67 | clients := []*MockClient{} 68 | var wg sync.WaitGroup 69 | 70 | for userName := range users { 71 | client := &MockClient{errs: []error{}} 72 | clients = append(clients, client) 73 | wg.Add(1) 74 | go client.uploadAndDownload(t, addr, userName, userPwd, filesCount, &wg) 75 | } 76 | 77 | wg.Wait() 78 | 79 | errs := []error{} 80 | for _, client := range clients { 81 | if len(client.errs) > 0 { 82 | errs = append(errs, client.errs...) 83 | } 84 | } 85 | if len(errs) > 0 { 86 | t.Fatal(joinErrs(errs)) 87 | } 88 | } 89 | 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /src/server/testdata/config_1.yml: -------------------------------------------------------------------------------- 1 | fs: 2 | root: "1" 3 | opensLimit: 1 4 | openTTL: 1 5 | publicPath: "1" 6 | secrets: 7 | tokenSecret: "1" 8 | server: 9 | debug: true 10 | host: "1" 11 | port: 1 12 | readTimeout: 1 13 | writeTimeout: 1 14 | maxHeaderBytes: 1 15 | users: 16 | enableAuth: true 17 | defaultAdmin: "1" 18 | defaultAdminPwd: "1" 19 | cookieTTL: 1 20 | cookieSecure: true 21 | cookieHttpOnly: true 22 | minUserNameLen: 1 23 | minPwdLen: 1 24 | captchaWidth: 1 25 | captchaHeight: 1 26 | captchaEnabled: true 27 | uploadSpeedLimit: 1 28 | downloadSpeedLimit: 1 29 | spaceLimit: 1 30 | limiterCapacity: 1 31 | limiterCyc: 1 32 | predefinedUsers: 33 | - name: "1" 34 | pwd: "1" 35 | role: "1" 36 | workers: 37 | queueSize: 1 38 | sleepCyc: 1 39 | workerCount: 1 40 | # site: 41 | # clientCfg: 42 | # siteName: "1" 43 | # siteDesc: "1" 44 | # bg: 45 | # url: "1" 46 | # repeat: "1" 47 | # position: "1" 48 | # align: "1" 49 | # bgColor: "1" 50 | db: 51 | dbPath: "testdata/quickshare.sqlite" 52 | 53 | -------------------------------------------------------------------------------- /src/server/testdata/config_4.yml: -------------------------------------------------------------------------------- 1 | fs: 2 | root: "4" 3 | opensLimit: 4 4 | openTTL: 4 5 | publicPath: "4" 6 | secrets: 7 | tokenSecret: "4" 8 | server: 9 | debug: false 10 | host: "4" 11 | port: 4 12 | readTimeout: 4 13 | writeTimeout: 4 14 | maxHeaderBytes: 4 15 | dynamic: 16 | clientCfg: 17 | siteName: "4" 18 | siteDesc: "4" 19 | bg: 20 | url: "4" 21 | repeat: "4" 22 | position: "4" 23 | align: "4" 24 | bgColor: "4" 25 | allowSetBg: true 26 | autoTheme: true 27 | users: 28 | enableAuth: false 29 | defaultAdmin: "4" 30 | defaultAdminPwd: "4" 31 | cookieTTL: 4 32 | cookieSecure: false 33 | cookieHttpOnly: false 34 | minUserNameLen: 4 35 | minPwdLen: 4 36 | captchaWidth: 4 37 | captchaHeight: 4 38 | captchaEnabled: false 39 | uploadSpeedLimit: 4 40 | downloadSpeedLimit: 4 41 | spaceLimit: 4 42 | limiterCapacity: 4 43 | limiterCyc: 4 44 | predefinedUsers: 45 | - name: "4" 46 | pwd: "4" 47 | role: "4" 48 | workers: 49 | queueSize: 4 50 | sleepCyc: 4 51 | workerCount: 4 52 | db: 53 | dbPath: "4" 54 | -------------------------------------------------------------------------------- /src/server/testdata/config_partial_db.yml: -------------------------------------------------------------------------------- 1 | db: 2 | dbPath: "5" -------------------------------------------------------------------------------- /src/server/testdata/config_partial_users.yml: -------------------------------------------------------------------------------- 1 | users: 2 | enableAuth: true 3 | defaultAdmin: "5" 4 | defaultAdminPwd: "5" 5 | # cookieTTL: 5 6 | cookieSecure: true 7 | cookieHttpOnly: true 8 | minUserNameLen: 5 9 | minPwdLen: 5 10 | captchaWidth: 5 11 | captchaHeight: 5 12 | captchaEnabled: true 13 | uploadSpeedLimit: 5 14 | downloadSpeedLimit: 5 15 | spaceLimit: 5 16 | limiterCapacity: 5 17 | limiterCyc: 5 18 | predefinedUsers: 19 | - name: "5" 20 | pwd: "5" 21 | role: "5" 22 | -------------------------------------------------------------------------------- /src/server/testdata/quickshare.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/src/server/testdata/quickshare.sqlite -------------------------------------------------------------------------------- /src/worker/interface.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrFull = errors.New("worker queue is full, make it larger in the config") 7 | ErrClosed = errors.New("async handlers are closed") 8 | ) 9 | 10 | func IsErrFull(err error) bool { 11 | return err == ErrFull 12 | } 13 | 14 | type IMsg interface { 15 | ID() uint64 16 | Headers() map[string]string 17 | Body() string 18 | } 19 | 20 | type MsgHandler = func(msg IMsg) error 21 | 22 | type IWorkerPool interface { 23 | TryPut(task IMsg) error 24 | Start() 25 | Stop() 26 | AddHandler(msgType string, handler MsgHandler) 27 | DelHandler(msgType string) 28 | QueueLen() int 29 | } 30 | -------------------------------------------------------------------------------- /src/worker/localworker/worker.go: -------------------------------------------------------------------------------- 1 | package localworker 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "go.uber.org/zap" 9 | 10 | "github.com/ihexxa/quickshare/src/worker" 11 | ) 12 | 13 | // TODO: support context 14 | 15 | const ( 16 | MsgTypeKey = "msg-type" 17 | ) 18 | 19 | type Msg struct { 20 | id uint64 21 | headers map[string]string 22 | body string 23 | } 24 | 25 | func NewMsg(id uint64, headers map[string]string, body string) *Msg { 26 | return &Msg{ 27 | id: id, 28 | headers: headers, 29 | body: body, 30 | } 31 | } 32 | 33 | func (m *Msg) ID() uint64 { 34 | return m.id 35 | } 36 | 37 | func (m *Msg) Headers() map[string]string { 38 | return m.headers 39 | } 40 | 41 | func (m *Msg) Body() string { 42 | return m.body 43 | } 44 | 45 | type WorkerPool struct { 46 | on bool 47 | listening bool 48 | queue chan worker.IMsg 49 | sleep int 50 | workerCount int 51 | started int 52 | mtx *sync.RWMutex 53 | logger *zap.SugaredLogger 54 | msgHandlers map[string]worker.MsgHandler 55 | } 56 | 57 | func NewWorkerPool(queueSize, sleep, workerCount int, logger *zap.SugaredLogger) *WorkerPool { 58 | return &WorkerPool{ 59 | on: true, 60 | listening: true, 61 | logger: logger, 62 | mtx: &sync.RWMutex{}, 63 | sleep: sleep, 64 | workerCount: workerCount, 65 | queue: make(chan worker.IMsg, queueSize), 66 | msgHandlers: map[string]worker.MsgHandler{}, 67 | } 68 | } 69 | 70 | func (wp *WorkerPool) TryPut(task worker.IMsg) error { 71 | // this closes the window that queue can be full after checking 72 | wp.mtx.Lock() 73 | defer wp.mtx.Unlock() 74 | 75 | if !wp.listening { 76 | return worker.ErrClosed 77 | } 78 | if len(wp.queue) == cap(wp.queue) { 79 | return worker.ErrFull 80 | } 81 | wp.queue <- task 82 | return nil 83 | } 84 | 85 | func (wp *WorkerPool) Start() { 86 | wp.mtx.Lock() 87 | defer wp.mtx.Unlock() 88 | 89 | wp.on = true 90 | wp.listening = true 91 | for wp.started < wp.workerCount { 92 | go wp.startWorker() 93 | wp.started++ 94 | } 95 | } 96 | 97 | func (wp *WorkerPool) Stop() { 98 | wp.mtx.Lock() 99 | defer wp.mtx.Unlock() 100 | 101 | wp.listening = false 102 | 103 | // TODO: avoid sending and panic 104 | for len(wp.queue) > 0 { 105 | wp.logger.Infof( 106 | fmt.Sprintf( 107 | "draining: %d messages left", 108 | len(wp.queue), 109 | ), 110 | ) 111 | time.Sleep(time.Duration(1) * time.Second) 112 | } 113 | close(wp.queue) 114 | 115 | wp.on = false 116 | for wp.started > 0 { 117 | wp.logger.Infof( 118 | fmt.Sprintf( 119 | "stopping: %d workers (sleep %d second) still in working/sleeping", 120 | wp.sleep, 121 | wp.started, 122 | ), 123 | ) 124 | time.Sleep(time.Duration(1) * time.Second) 125 | } 126 | } 127 | 128 | func (wp *WorkerPool) startWorker() { 129 | var err error 130 | 131 | // TODO: make it stateful 132 | for wp.on { 133 | func() { 134 | defer func() { 135 | if p := recover(); p != nil { 136 | wp.logger.Errorf("worker panic: %s", p) 137 | } 138 | }() 139 | 140 | msg, ok := <-wp.queue 141 | if !ok { 142 | return 143 | } 144 | 145 | headers := msg.Headers() 146 | msgType, ok := headers[MsgTypeKey] 147 | if !ok { 148 | wp.logger.Errorf("msg type not found: %v", headers) 149 | return 150 | } 151 | 152 | handler, ok := wp.msgHandlers[msgType] 153 | if !ok { 154 | wp.logger.Errorf("no handler for the message type: %s", msgType) 155 | return 156 | } 157 | 158 | if err = handler(msg); err != nil { 159 | wp.logger.Errorf("async task(%s) failed: %s", msgType, err) 160 | } 161 | }() 162 | 163 | time.Sleep(time.Duration(wp.sleep) * time.Second) 164 | } 165 | 166 | wp.started-- 167 | } 168 | 169 | func (wp *WorkerPool) AddHandler(msgType string, handler worker.MsgHandler) { 170 | // existing task type will be overwritten 171 | wp.msgHandlers[msgType] = handler 172 | } 173 | 174 | func (wp *WorkerPool) DelHandler(msgType string) { 175 | delete(wp.msgHandlers, msgType) 176 | } 177 | 178 | func (wp *WorkerPool) QueueLen() int { 179 | return len(wp.queue) 180 | } 181 | -------------------------------------------------------------------------------- /src/worker/localworker/worker_test.go: -------------------------------------------------------------------------------- 1 | package localworker_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "sync" 7 | "testing" 8 | 9 | "go.uber.org/zap" 10 | "go.uber.org/zap/zapcore" 11 | 12 | "github.com/ihexxa/quickshare/src/worker" 13 | "github.com/ihexxa/quickshare/src/worker/localworker" 14 | ) 15 | 16 | func TestWorkerPools(t *testing.T) { 17 | type tinput struct { 18 | ID int `json:"id"` 19 | } 20 | 21 | workersTest := func(workers worker.IWorkerPool, t *testing.T) { 22 | records := &sync.Map{} 23 | mType1, mType2 := "mtype1", "mtype2" 24 | 25 | handler1 := func(msg worker.IMsg) error { 26 | input := &tinput{} 27 | err := json.Unmarshal([]byte(msg.Body()), input) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | records.Store(mType1, input.ID) 33 | return nil 34 | } 35 | handler2 := func(msg worker.IMsg) error { 36 | input := &tinput{} 37 | err := json.Unmarshal([]byte(msg.Body()), input) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | records.Store(mType2, input.ID) 43 | return nil 44 | } 45 | 46 | workers.AddHandler(mType1, handler1) 47 | workers.AddHandler(mType2, handler2) 48 | workers.Start() 49 | 50 | count := 3 51 | for i := 0; i < count; i++ { 52 | body, _ := json.Marshal(&tinput{ID: i}) 53 | workers.TryPut(localworker.NewMsg( 54 | uint64(i), 55 | map[string]string{localworker.MsgTypeKey: mType1}, 56 | string(body), 57 | )) 58 | workers.TryPut(localworker.NewMsg( 59 | uint64(i*10), 60 | map[string]string{localworker.MsgTypeKey: mType2}, 61 | string(body), 62 | )) 63 | } 64 | 65 | workers.Stop() 66 | workers.DelHandler(mType1) 67 | workers.DelHandler(mType2) 68 | 69 | val1, ok := records.Load(mType1) 70 | if !ok { 71 | t.Fatal("mtype1 not found") 72 | } 73 | count1 := val1.(int) 74 | if count1 != count-1 { 75 | t.Fatalf("incorrect count %d", count1) 76 | } 77 | 78 | val2, ok := records.Load(mType2) 79 | if !ok { 80 | t.Fatal("mtype2 not found") 81 | } 82 | count2 := val2.(int) 83 | if count1 != count-1 { 84 | t.Fatalf("incorrect count %d", count2) 85 | } 86 | } 87 | 88 | t.Run("test bolt provider", func(t *testing.T) { 89 | // rootPath, err := ioutil.TempDir("./", "quickshare_kvstore_test_") 90 | // if err != nil { 91 | // t.Fatal(err) 92 | // } 93 | // defer os.RemoveAll(rootPath) 94 | 95 | stdoutWriter := zapcore.AddSync(os.Stdout) 96 | multiWriter := zapcore.NewMultiWriteSyncer(stdoutWriter) 97 | core := zapcore.NewCore( 98 | zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), 99 | multiWriter, 100 | zap.InfoLevel, 101 | ) 102 | 103 | workers := localworker.NewWorkerPool(1024, 1, 2, zap.New(core).Sugar()) 104 | workersTest(workers, t) 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /static/fs.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | ) 8 | 9 | //go:embed public/* 10 | var embedFS embed.FS 11 | 12 | type EmbedStaticFS struct { 13 | http.FileSystem 14 | } 15 | 16 | func NewEmbedStaticFS() (*EmbedStaticFS, error) { 17 | // the public folder will temporarily be copied to here in building 18 | publicFS, err := fs.Sub(embedFS, "public") 19 | if err != nil { 20 | return nil, err 21 | } 22 | httpFS := http.FS(publicFS) 23 | 24 | return &EmbedStaticFS{ 25 | FileSystem: httpFS, 26 | }, nil 27 | } 28 | 29 | func (efs *EmbedStaticFS) Exists(prefix string, path string) bool { 30 | // prefix should already be considered by http.FileSystem 31 | _, err := efs.Open(path) 32 | if err != nil { 33 | // TODO: need more checking 34 | // if errors.Is(err, fs.ErrNotExist) { 35 | return false 36 | } 37 | return true 38 | } 39 | -------------------------------------------------------------------------------- /static/public/css/colors.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | .blue0-font { 4 | color: #3498db; 5 | } 6 | 7 | .blue1-font { 8 | color: #2980b9; 9 | } 10 | 11 | .cyan0-font { 12 | color: #1abc9c; 13 | } 14 | 15 | .cyan1-font { 16 | color: #16a085; 17 | } 18 | 19 | .purple0-font { 20 | color: #9b59b6; 21 | } 22 | 23 | .purple1-font { 24 | color: #8e44ad; 25 | } 26 | 27 | .red0-font { 28 | color: #e74c3c; 29 | } 30 | 31 | .red1-font { 32 | color: #c0392b; 33 | } 34 | 35 | .yellow0-font { 36 | color: #f1c40f; 37 | } 38 | 39 | .yellow1-font { 40 | color: #f39c12; 41 | } 42 | 43 | .yellow2-font { 44 | color: #e67e22; 45 | } 46 | 47 | .yellow3-font { 48 | color: #d35400; 49 | } 50 | 51 | .green0-font { 52 | color: #2ecc71; 53 | } 54 | 55 | .green1-font { 56 | color: #27ae60; 57 | } 58 | 59 | .green2-font { 60 | color: #15cd3d; 61 | } 62 | 63 | .white-font { 64 | color: #fff; 65 | } 66 | 67 | .white0-font { 68 | color: #ecf0f1; 69 | } 70 | 71 | .white1-font { 72 | color: #bdc3c7; 73 | } 74 | 75 | .grey0-font { 76 | color: #95a5a6; 77 | } 78 | 79 | .grey1-font { 80 | color: #7f8c8d; 81 | } 82 | 83 | .grey2-font { 84 | color: #ecf0f6; 85 | } 86 | 87 | .grey3-font { 88 | color: #697384; 89 | } 90 | 91 | .black-font { 92 | color: #000; 93 | } 94 | 95 | .black0-font { 96 | color: #34495e; 97 | } 98 | 99 | .black1-font { 100 | color: #2c3e50; 101 | } 102 | 103 | .blue0-bg { 104 | background-color: #3498db; 105 | } 106 | 107 | .blue1-bg { 108 | background-color: #2980b9; 109 | } 110 | 111 | .blue2-bg { 112 | background-color: #2f45c5; 113 | } 114 | 115 | .cyan0-bg { 116 | background-color: #1abc9c; 117 | } 118 | 119 | .cyan1-bg { 120 | background-color: #16a085; 121 | } 122 | 123 | .purple0-bg { 124 | background-color: #9b59b6; 125 | } 126 | 127 | .purple1-bg { 128 | background-color: #8e44ad; 129 | } 130 | 131 | .red0-bg { 132 | background-color: #e74c3c; 133 | } 134 | 135 | .red1-bg { 136 | background-color: #c0392b; 137 | } 138 | 139 | .yellow0-bg { 140 | background-color: #f1c40f; 141 | } 142 | 143 | .yellow1-bg { 144 | background-color: #f39c12; 145 | } 146 | 147 | .yellow2-bg { 148 | background-color: #e67e22; 149 | } 150 | 151 | .yellow3-bg { 152 | background-color: #15cd3d; 153 | } 154 | 155 | .yellow3-bg { 156 | background-color: #d35400; 157 | } 158 | 159 | .green0-bg { 160 | background-color: #2ecc71; 161 | } 162 | 163 | .green1-bg { 164 | background-color: #27ae60; 165 | } 166 | 167 | .green2-bg { 168 | background-color: #15cd3d; 169 | } 170 | 171 | .white-bg { 172 | background-color: #fff; 173 | } 174 | 175 | .white0-bg { 176 | background-color: #ecf0f1; 177 | } 178 | 179 | .white1-bg { 180 | background-color: #bdc3c7; 181 | } 182 | 183 | .grey0-bg { 184 | background-color: #95a5a6; 185 | } 186 | 187 | .grey1-bg { 188 | background-color: #7f8c8d; 189 | } 190 | .grey2-bg { 191 | background-color: #ecf0f6; 192 | } 193 | 194 | .grey3-bg { 195 | background-color: #697384; 196 | } 197 | 198 | .black-bg { 199 | background-color: #000; 200 | } 201 | 202 | .black0-bg { 203 | background-color: #34495e; 204 | } 205 | 206 | .black1-bg { 207 | background-color: #2c3e50; 208 | } 209 | -------------------------------------------------------------------------------- /static/public/css/reset.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html, 4 | body, 5 | p, 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | h6 { 12 | margin: 0; 13 | outline: 0; 14 | padding: 0; 15 | border: 0; 16 | } 17 | html { 18 | background-color: #ecf0f1; 19 | font-family: "Helvetica Neue", "Helvetica", "PingFang SC", "Microsoft YaHei", 20 | "Arial", sans-serif; 21 | font-size: 62.5%; 22 | } 23 | *:focus { 24 | outline: none; 25 | } 26 | 27 | a, 28 | a:link, 29 | a:visited, 30 | a:hover, 31 | a:active, 32 | button, 33 | img { 34 | -webkit-touch-callout: none; /* iOS Safari */ 35 | -webkit-user-select: none; /* Safari */ 36 | -khtml-user-select: none; /* Konqueror HTML */ 37 | -moz-user-select: none; /* Firefox */ 38 | -ms-user-select: none; /* Internet Explorer/Edge */ 39 | user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ 40 | } 41 | a { 42 | opacity: 100%; 43 | text-decoration: none; 44 | transition: color 300ms; 45 | } 46 | a::selection { 47 | background: transparent; 48 | } 49 | 50 | span, 51 | div, 52 | svg { 53 | transition: color 300ms, background-color 300ms; 54 | } 55 | 56 | img { 57 | max-width: 100%; 58 | } 59 | 60 | table { 61 | border-collapse: collapse; 62 | border-spacing: 0; 63 | } 64 | 65 | input { 66 | font-size: 1.2rem; 67 | line-height: 3.2rem; 68 | height: 3.2rem; 69 | padding: 0.8rem 1rem; 70 | transition: opacity 300ms, box-shadow 300ms; 71 | } 72 | 73 | input:focus { 74 | opacity: 0.8; 75 | } 76 | 77 | button { 78 | cursor: pointer; 79 | 80 | font-size: 1.2rem; 81 | line-height: 3.2rem; 82 | border: none; 83 | outline: none; 84 | height: 3.2rem; 85 | 86 | transition: opacity 300ms; 87 | } 88 | button:hover { 89 | opacity: 0.8; 90 | } 91 | button:active { 92 | opacity: 1; 93 | } 94 | -------------------------------------------------------------------------------- /static/public/font/Hurricane-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/static/public/font/Hurricane-Regular.ttf -------------------------------------------------------------------------------- /static/public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/static/public/img/favicon.png -------------------------------------------------------------------------------- /static/public/img/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/public/img/prism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/static/public/img/prism.png -------------------------------------------------------------------------------- /static/public/img/px_by_Gre3g.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/static/public/img/px_by_Gre3g.webp -------------------------------------------------------------------------------- /static/public/img/textured_paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/static/public/img/textured_paper.png -------------------------------------------------------------------------------- /static/public/img/tweed.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihexxa/quickshare/ec926209d2aa8cbf81919d72f427a7e682e5c0b5/static/public/img/tweed.webp -------------------------------------------------------------------------------- /static/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quickshare", 3 | "short_name": "Quickshare", 4 | "icons": [{ 5 | "src": "img/favicon.png", 6 | "sizes": "512x512" 7 | }], 8 | "background_color": "#16a085", 9 | "theme_color": "#ffffff", 10 | "display": "fullscreen" 11 | } 12 | --------------------------------------------------------------------------------