├── .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 | 
4 | 
5 |
6 | # v0.5.1
7 | ## Login
8 | 
9 |
10 | # v0.4.20
11 |
12 | ## Site Address QR code
13 |
14 | 
15 |
16 | ## Files Panel
17 |
18 | 
19 | 
20 | 
21 |
22 | ## All Panels
23 |
24 | 
25 |
26 | ## Settings
27 |
28 | 
29 |
30 | ## Management
31 |
32 | 
33 | 
34 |
35 | ## Sharing Panel
36 |
37 | 
38 | 
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 |
69 |
70 |
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 |
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 |
47 | ) : null;
48 |
49 | return (
50 |
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 |
--------------------------------------------------------------------------------