├── .github └── workflows │ └── build_release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE-MIT ├── README.md ├── README_cn.md ├── backend ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── assets │ ├── migrations │ │ ├── 20210829_init_db.sql │ │ ├── 20211224_guest_login.sql │ │ └── 20220106_reset_password.sql │ ├── oasis.conf.sample │ └── tests │ │ ├── 01.srt │ │ └── tls │ │ ├── rsa_sha256_cert.pem │ │ └── rsa_sha256_key.pem └── src │ ├── api │ ├── files.rs │ ├── mod.rs │ ├── sys.rs │ ├── upload.rs │ └── user.rs │ ├── entity │ ├── copy_move_task.rs │ ├── error.rs │ ├── file.rs │ ├── hidden.rs │ ├── mod.rs │ ├── request.rs │ ├── reset_password.rs │ ├── response.rs │ ├── site.rs │ ├── upload_task.rs │ └── user.rs │ ├── main.rs │ ├── service │ ├── app_state.rs │ ├── auth.rs │ ├── fairings.rs │ ├── migrate_dir.rs │ ├── mod.rs │ ├── range.rs │ ├── static_route.rs │ ├── token.rs │ └── track.rs │ └── util │ ├── constants.rs │ ├── db.rs │ ├── file_system.rs │ ├── init.rs │ ├── local_ip.rs │ ├── mod.rs │ └── rocket_env.rs ├── build.js ├── doc └── demo.png ├── docker ├── Dockerfile └── oasis-v0.2-alpine.zip ├── frontend ├── .gitignore ├── .vscode │ └── extensions.json ├── nodemon.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.png │ ├── global.css │ ├── index.html │ ├── js │ │ ├── md5_worker.js │ │ └── upload_worker.js │ └── vendor │ │ ├── md5 │ │ └── md5.js │ │ ├── pdfjs │ │ ├── LICENSE │ │ ├── build │ │ │ ├── pdf.js │ │ │ ├── pdf.sandbox.js │ │ │ └── pdf.worker.js │ │ └── web │ │ │ ├── debugger.js │ │ │ ├── images │ │ │ ├── annotation-check.svg │ │ │ ├── annotation-comment.svg │ │ │ ├── annotation-help.svg │ │ │ ├── annotation-insert.svg │ │ │ ├── annotation-key.svg │ │ │ ├── annotation-newparagraph.svg │ │ │ ├── annotation-noicon.svg │ │ │ ├── annotation-note.svg │ │ │ ├── annotation-paragraph.svg │ │ │ ├── findbarButton-next.svg │ │ │ ├── findbarButton-previous.svg │ │ │ ├── grab.cur │ │ │ ├── grabbing.cur │ │ │ ├── loading-dark.svg │ │ │ ├── loading-icon.gif │ │ │ ├── loading.svg │ │ │ ├── secondaryToolbarButton-documentProperties.svg │ │ │ ├── secondaryToolbarButton-firstPage.svg │ │ │ ├── secondaryToolbarButton-handTool.svg │ │ │ ├── secondaryToolbarButton-lastPage.svg │ │ │ ├── secondaryToolbarButton-rotateCcw.svg │ │ │ ├── secondaryToolbarButton-rotateCw.svg │ │ │ ├── secondaryToolbarButton-scrollHorizontal.svg │ │ │ ├── secondaryToolbarButton-scrollVertical.svg │ │ │ ├── secondaryToolbarButton-scrollWrapped.svg │ │ │ ├── secondaryToolbarButton-selectTool.svg │ │ │ ├── secondaryToolbarButton-spreadEven.svg │ │ │ ├── secondaryToolbarButton-spreadNone.svg │ │ │ ├── secondaryToolbarButton-spreadOdd.svg │ │ │ ├── shadow.png │ │ │ ├── toolbarButton-bookmark.svg │ │ │ ├── toolbarButton-currentOutlineItem.svg │ │ │ ├── toolbarButton-download.svg │ │ │ ├── toolbarButton-menuArrow.svg │ │ │ ├── toolbarButton-openFile.svg │ │ │ ├── toolbarButton-pageDown.svg │ │ │ ├── toolbarButton-pageUp.svg │ │ │ ├── toolbarButton-presentationMode.svg │ │ │ ├── toolbarButton-print.svg │ │ │ ├── toolbarButton-search.svg │ │ │ ├── toolbarButton-secondaryToolbarToggle.svg │ │ │ ├── toolbarButton-sidebarToggle.svg │ │ │ ├── toolbarButton-viewAttachments.svg │ │ │ ├── toolbarButton-viewLayers.svg │ │ │ ├── toolbarButton-viewOutline.svg │ │ │ ├── toolbarButton-viewThumbnail.svg │ │ │ ├── toolbarButton-zoomIn.svg │ │ │ ├── toolbarButton-zoomOut.svg │ │ │ ├── treeitem-collapsed.svg │ │ │ └── treeitem-expanded.svg │ │ │ ├── locale │ │ │ ├── ach │ │ │ │ └── viewer.properties │ │ │ ├── af │ │ │ │ └── viewer.properties │ │ │ ├── an │ │ │ │ └── viewer.properties │ │ │ ├── ar │ │ │ │ └── viewer.properties │ │ │ ├── ast │ │ │ │ └── viewer.properties │ │ │ ├── az │ │ │ │ └── viewer.properties │ │ │ ├── be │ │ │ │ └── viewer.properties │ │ │ ├── bg │ │ │ │ └── viewer.properties │ │ │ ├── bn │ │ │ │ └── viewer.properties │ │ │ ├── bo │ │ │ │ └── viewer.properties │ │ │ ├── br │ │ │ │ └── viewer.properties │ │ │ ├── brx │ │ │ │ └── viewer.properties │ │ │ ├── bs │ │ │ │ └── viewer.properties │ │ │ ├── ca │ │ │ │ └── viewer.properties │ │ │ ├── cak │ │ │ │ └── viewer.properties │ │ │ ├── ckb │ │ │ │ └── viewer.properties │ │ │ ├── cs │ │ │ │ └── viewer.properties │ │ │ ├── cy │ │ │ │ └── viewer.properties │ │ │ ├── da │ │ │ │ └── viewer.properties │ │ │ ├── de │ │ │ │ └── viewer.properties │ │ │ ├── dsb │ │ │ │ └── viewer.properties │ │ │ ├── el │ │ │ │ └── viewer.properties │ │ │ ├── en-CA │ │ │ │ └── viewer.properties │ │ │ ├── en-GB │ │ │ │ └── viewer.properties │ │ │ ├── en-US │ │ │ │ └── viewer.properties │ │ │ ├── eo │ │ │ │ └── viewer.properties │ │ │ ├── es-AR │ │ │ │ └── viewer.properties │ │ │ ├── es-CL │ │ │ │ └── viewer.properties │ │ │ ├── es-ES │ │ │ │ └── viewer.properties │ │ │ ├── es-MX │ │ │ │ └── viewer.properties │ │ │ ├── et │ │ │ │ └── viewer.properties │ │ │ ├── eu │ │ │ │ └── viewer.properties │ │ │ ├── fa │ │ │ │ └── viewer.properties │ │ │ ├── ff │ │ │ │ └── viewer.properties │ │ │ ├── fi │ │ │ │ └── viewer.properties │ │ │ ├── fr │ │ │ │ └── viewer.properties │ │ │ ├── fy-NL │ │ │ │ └── viewer.properties │ │ │ ├── ga-IE │ │ │ │ └── viewer.properties │ │ │ ├── gd │ │ │ │ └── viewer.properties │ │ │ ├── gl │ │ │ │ └── viewer.properties │ │ │ ├── gn │ │ │ │ └── viewer.properties │ │ │ ├── gu-IN │ │ │ │ └── viewer.properties │ │ │ ├── he │ │ │ │ └── viewer.properties │ │ │ ├── hi-IN │ │ │ │ └── viewer.properties │ │ │ ├── hr │ │ │ │ └── viewer.properties │ │ │ ├── hsb │ │ │ │ └── viewer.properties │ │ │ ├── hu │ │ │ │ └── viewer.properties │ │ │ ├── hy-AM │ │ │ │ └── viewer.properties │ │ │ ├── hye │ │ │ │ └── viewer.properties │ │ │ ├── ia │ │ │ │ └── viewer.properties │ │ │ ├── id │ │ │ │ └── viewer.properties │ │ │ ├── is │ │ │ │ └── viewer.properties │ │ │ ├── it │ │ │ │ └── viewer.properties │ │ │ ├── ja │ │ │ │ └── viewer.properties │ │ │ ├── ka │ │ │ │ └── viewer.properties │ │ │ ├── kab │ │ │ │ └── viewer.properties │ │ │ ├── kk │ │ │ │ └── viewer.properties │ │ │ ├── km │ │ │ │ └── viewer.properties │ │ │ ├── kn │ │ │ │ └── viewer.properties │ │ │ ├── ko │ │ │ │ └── viewer.properties │ │ │ ├── lij │ │ │ │ └── viewer.properties │ │ │ ├── lo │ │ │ │ └── viewer.properties │ │ │ ├── locale.properties │ │ │ ├── lt │ │ │ │ └── viewer.properties │ │ │ ├── ltg │ │ │ │ └── viewer.properties │ │ │ ├── lv │ │ │ │ └── viewer.properties │ │ │ ├── meh │ │ │ │ └── viewer.properties │ │ │ ├── mk │ │ │ │ └── viewer.properties │ │ │ ├── mr │ │ │ │ └── viewer.properties │ │ │ ├── ms │ │ │ │ └── viewer.properties │ │ │ ├── my │ │ │ │ └── viewer.properties │ │ │ ├── nb-NO │ │ │ │ └── viewer.properties │ │ │ ├── ne-NP │ │ │ │ └── viewer.properties │ │ │ ├── nl │ │ │ │ └── viewer.properties │ │ │ ├── nn-NO │ │ │ │ └── viewer.properties │ │ │ ├── oc │ │ │ │ └── viewer.properties │ │ │ ├── pa-IN │ │ │ │ └── viewer.properties │ │ │ ├── pl │ │ │ │ └── viewer.properties │ │ │ ├── pt-BR │ │ │ │ └── viewer.properties │ │ │ ├── pt-PT │ │ │ │ └── viewer.properties │ │ │ ├── rm │ │ │ │ └── viewer.properties │ │ │ ├── ro │ │ │ │ └── viewer.properties │ │ │ ├── ru │ │ │ │ └── viewer.properties │ │ │ ├── scn │ │ │ │ └── viewer.properties │ │ │ ├── sco │ │ │ │ └── viewer.properties │ │ │ ├── si │ │ │ │ └── viewer.properties │ │ │ ├── sk │ │ │ │ └── viewer.properties │ │ │ ├── sl │ │ │ │ └── viewer.properties │ │ │ ├── son │ │ │ │ └── viewer.properties │ │ │ ├── sq │ │ │ │ └── viewer.properties │ │ │ ├── sr │ │ │ │ └── viewer.properties │ │ │ ├── sv-SE │ │ │ │ └── viewer.properties │ │ │ ├── szl │ │ │ │ └── viewer.properties │ │ │ ├── ta │ │ │ │ └── viewer.properties │ │ │ ├── te │ │ │ │ └── viewer.properties │ │ │ ├── tg │ │ │ │ └── viewer.properties │ │ │ ├── th │ │ │ │ └── viewer.properties │ │ │ ├── tl │ │ │ │ └── viewer.properties │ │ │ ├── tr │ │ │ │ └── viewer.properties │ │ │ ├── trs │ │ │ │ └── viewer.properties │ │ │ ├── uk │ │ │ │ └── viewer.properties │ │ │ ├── ur │ │ │ │ └── viewer.properties │ │ │ ├── uz │ │ │ │ └── viewer.properties │ │ │ ├── vi │ │ │ │ └── viewer.properties │ │ │ ├── wo │ │ │ │ └── viewer.properties │ │ │ ├── xh │ │ │ │ └── viewer.properties │ │ │ ├── zh-CN │ │ │ │ └── viewer.properties │ │ │ └── zh-TW │ │ │ │ └── viewer.properties │ │ │ ├── viewer.css │ │ │ ├── viewer.html │ │ │ └── viewer.js │ │ └── plyr │ │ └── plyr.css ├── rollup.config.js ├── src │ ├── App.svelte │ ├── assets │ │ ├── constants.json │ │ └── i18n │ │ │ ├── cn.json │ │ │ └── en.json │ ├── components │ │ ├── BreadCrum.svelte │ │ ├── Button.svelte │ │ ├── FileIcon.svelte │ │ ├── Icon.svelte │ │ ├── Modal.svelte │ │ ├── Spinner.svelte │ │ ├── Switch.svelte │ │ └── Tailwind.svelte │ ├── global.d.ts │ ├── i18n.js │ ├── main.ts │ ├── modals │ │ ├── AboutModal.svelte │ │ ├── CopyMoveFileModal.svelte │ │ ├── DeleteFileModal.svelte │ │ ├── FileLinkModal.svelte │ │ ├── FileVisibilityModal.svelte │ │ ├── NewFilenameModal.svelte │ │ ├── PromptModal.svelte │ │ ├── SearchModal.svelte │ │ ├── ShutdownModal.svelte │ │ ├── SignOutModal.svelte │ │ └── UpdateModal.svelte │ ├── pages │ │ ├── DirList.svelte │ │ ├── FileView.svelte │ │ ├── Files.svelte │ │ ├── ForgotPassword.svelte │ │ ├── Home.svelte │ │ ├── Login.svelte │ │ ├── Profile.svelte │ │ ├── ResetPassword.svelte │ │ ├── Settings.svelte │ │ └── Setup.svelte │ ├── players │ │ ├── ImageViewer.svelte │ │ ├── MediaPlayer.svelte │ │ ├── PdfViewer.svelte │ │ └── TextViewer.svelte │ ├── sections │ │ ├── AvatarMenu.svelte │ │ ├── ContextMenu.svelte │ │ ├── DirBrowser.svelte │ │ ├── FilesList.svelte │ │ ├── Header.svelte │ │ ├── Notification.svelte │ │ ├── Title.svelte │ │ └── UploadList.svelte │ └── utils │ │ ├── api.ts │ │ ├── enums.ts │ │ ├── store.ts │ │ ├── types.ts │ │ ├── upload.ts │ │ └── util.ts ├── tailwind.config.js └── tsconfig.json └── version.txt /.github/workflows/build_release.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | binaries: 9 | name: ${{ matrix.os }} build 10 | runs-on: ${{ matrix.os }} 11 | timeout-minutes: 30 12 | strategy: 13 | matrix: 14 | include: 15 | - os: ubuntu-latest 16 | release_name: linux_x86_64 17 | - os: macos-latest 18 | release_name: macos_x86_64 19 | - os: windows-latest 20 | release_name: windows_x86_64 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | - name: Cache Dependencies 27 | uses: actions/cache@v2 28 | with: 29 | path: | 30 | ~/.cargo/registry 31 | ~/.cargo/git 32 | target 33 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 34 | 35 | - name: Setup Rust toolchain 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | toolchain: stable 39 | 40 | - name: Setup NodeJS toolchain 41 | uses: actions/setup-node@v2 42 | with: 43 | node-version: "14" 44 | cache: "npm" 45 | cache-dependency-path: frontend/package-lock.json 46 | 47 | - name: Build 48 | run: node build.js 49 | 50 | - name: Compress 51 | uses: vimtor/action-zip@v1 52 | with: 53 | files: release/ 54 | dest: ${{ matrix.release_name }}.zip 55 | 56 | - name: Get tag name 57 | id: tag_name 58 | run: | 59 | echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} 60 | shell: bash 61 | if: startsWith(github.ref, 'refs/tags/v') 62 | 63 | - name: Publish 64 | uses: svenstaro/upload-release-action@v2 65 | with: 66 | repo_token: ${{ secrets.GITHUB_TOKEN }} 67 | file: ${{ matrix.release_name }}.zip 68 | tag: ${{ github.ref }} 69 | asset_name: oasis-$tag-${{ matrix.release_name }}.zip 70 | if: startsWith(github.ref, 'refs/tags/v') 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # Next.js build output 78 | .next 79 | 80 | # Nuxt.js build / generate output 81 | .nuxt 82 | dist 83 | 84 | # Gatsby files 85 | .cache/ 86 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # public 89 | 90 | # vuepress build output 91 | .vuepress/dist 92 | 93 | # Serverless directories 94 | .serverless/ 95 | 96 | # FuseBox cache 97 | .fusebox/ 98 | 99 | # DynamoDB Local files 100 | .dynamodb/ 101 | 102 | # TernJS port file 103 | .tern-port 104 | 105 | **/release/** 106 | **/target/** 107 | **/oasis.exe 108 | **/a.txt 109 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### v0.2.6 4 | 5 | - Folder download 6 | 7 | ### v0.2.5 8 | 9 | - File copy 10 | - File move 11 | - Drag and drop upload 12 | 13 | ### v0.2.4 14 | 15 | - Reset password 16 | - Update hidden files when renaming or deleting 17 | 18 | This version uses directory `data/db` to store database files instead of `db`, which means: 19 | 20 | - Docker command should change accordingly, see `README.md` 21 | - Move `db` directory into `data` directory to keep previous data 22 | 23 | ### v0.2.3 24 | 25 | - Allow guest users login 26 | - Hide files from guest users 27 | 28 | ### v0.2.2 29 | 30 | - File search 31 | - Change modal style 32 | 33 | ### v0.2.1 34 | 35 | - Context menu 36 | - File rename 37 | - File delete 38 | - HTTPS via config file 39 | 40 | ### v0.2 41 | 42 | - File upload 43 | - Folder create 44 | - Character encoding other than UTF 45 | - Local IP address on other platforms 46 | 47 | ### v0.1.2 48 | 49 | - Optionally use config file oasis.conf to specify the IP address and port number 50 | - Prevent program crash when no local IP addresses are retrieved 51 | 52 | ### v0.1.1 53 | 54 | - External media player support via the shared link 55 | - File download 56 | - Refactor range request handler 57 | - Check app version against database version for update needs 58 | - Bug fix: token refresh frequency, update modal overflow 59 | 60 | ### v0.1 61 | 62 | - Initial version with basic functionalities. 63 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cheng Ma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## OASIS 2 | 3 | [![Build](https://github.com/machengim/oasis/actions/workflows/build_release.yml/badge.svg)](https://github.com/machengim/oasis/actions/workflows/build_release.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/machengim/oasis/blob/main/LICENSE-MIT) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/machengim/oasis)](https://github.com/machengim/oasis/releases) [![Docker](https://img.shields.io/badge/docker-v0.2.6-orange)](https://hub.docker.com/r/machengim/oasis) 4 | 5 | [中文 README](https://github.com/machengim/oasis/blob/main/README_cn.md) 6 | 7 | A self-hosted file server. 8 | 9 | ![](https://github.com/machengim/oasis/blob/main/doc/demo.png?raw=true) 10 | 11 | ### Install 12 | 13 | 1. Download from the [release](https://github.com/machengim/oasis/releases) page 14 | 2. Uncompress 15 | 3. Grant execute permission to `oasis` file if running in Linux or MacOS 16 | 4. (Optional) Config server IP and port number in `oasis.conf` file 17 | 5. Run `oasis` or `oasis.exe` 18 | 6. Visit the server's IP address in your favorite browser 19 | 20 | ### Docker 21 | 22 | https://hub.docker.com/r/machengim/oasis 23 | 24 | ``` 25 | docker run --name oasis -t -d \ 26 | -v :/opt/oasis/data \ 27 | -v :/home/storage \ 28 | -p :8000 machengim/oasis 29 | ``` 30 | 31 | ### Build 32 | 33 | - Node 14+ 34 | - Rust 1.54+ 35 | 36 | ``` 37 | cd path/to/oasis 38 | node build.js 39 | ``` 40 | 41 | ### Features 42 | 43 | - User authentication 44 | - File preview/download/upload/Search 45 | - Media file play list 46 | - File external link 47 | - I18n (English, Chinese) 48 | 49 | ### File format support 50 | 51 | - Text 52 | - Image (browser support) 53 | - Audio (browser support) 54 | - Video (browser support) 55 | - Subtitle (`srt` / `vtt` format, supported in Chrome, Firefox and Edge by now) 56 | - PDF (supported by pdf.js) 57 | 58 | ### Tech stack 59 | 60 | - [Svelte](https://svelte.dev) 61 | - [Rocket](https://rocket.rs) 62 | - [Tailwind](https://tailwindcss.com) 63 | 64 | ### Credits 65 | 66 | - [Pdf.js](https://mozilla.github.io/pdf.js) 67 | - [Plyr](https://plyr.io) 68 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | ## OASIS 2 | 3 | [![Build](https://github.com/machengim/oasis/actions/workflows/build_release.yml/badge.svg)](https://github.com/machengim/oasis/actions/workflows/build_release.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/machengim/oasis/blob/main/LICENSE-MIT) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/machengim/oasis)](https://github.com/machengim/oasis/releases) [![Docker](https://img.shields.io/badge/docker-v0.2.5-orange)](https://hub.docker.com/r/machengim/oasis) 4 | 5 | [English README](https://github.com/machengim/oasis/blob/main/README.md) 6 | 7 | 自建文件服务器。 8 | 9 | ![](https://github.com/machengim/oasis/blob/main/doc/demo.png?raw=true) 10 | 11 | ### 安装 12 | 13 | 1. 从 [release](https://github.com/machengim/oasis/releases) 页面下载 14 | 2. 解压缩 15 | 3. 如果运行在 Linux 或 MacOS 中, 授予 `oasis` 文件可执行权限 16 | 4. (可选) 在 `oasis.conf` 中设置服务器的 IP 和端口 17 | 5. 运行 `oasis` 或 `oasis.exe` 18 | 6. 从浏览器访问服务器的 IP 地址 19 | 20 | ### Docker 21 | 22 | https://hub.docker.com/r/machengim/oasis 23 | 24 | ``` 25 | docker run --name oasis -t -d \ 26 | -v :/opt/oasis/data \ 27 | -v :/home/storage \ 28 | -p :8000 machengim/oasis 29 | ``` 30 | 31 | ### 构建 32 | 33 | - Node 14+ 34 | - Rust 1.54+ 35 | 36 | ``` 37 | cd path/to/oasis 38 | node build.js 39 | ``` 40 | 41 | ### 功能 42 | 43 | - 用户验证 44 | - 文件预览/下载/上传/搜索 45 | - 媒体文件播放列表 46 | - 文件外部链接 47 | - I18n (英语, 中文) 48 | 49 | ### 文件格式支持 50 | 51 | - 文本 52 | - 图片 (浏览器支持) 53 | - 音频 (浏览器支持) 54 | - 视频 (浏览器支持) 55 | - 字幕 (`srt` / `vtt` 格式, 支持 Chrome, Firefox 和 Edge 浏览器) 56 | - PDF (由 pdf.js 支持) 57 | 58 | ### 技术栈 59 | 60 | - [Svelte](https://svelte.dev) 61 | - [Rocket](https://rocket.rs) 62 | - [Tailwind](https://tailwindcss.com) 63 | 64 | ### 致谢 65 | 66 | - [Pdf.js](https://mozilla.github.io/pdf.js) 67 | - [Plyr](https://plyr.io) 68 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/*.db* 3 | **/*.exe 4 | **/db/** -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oasis" 3 | version = "0.2.5" 4 | authors = ["Cheng "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | rocket = {version = "0.5.0-rc.1", features = ["json", "tls"]} 11 | anyhow = "1.0" 12 | async_zip = { version = "0.0.11", features = ["deflate"] } 13 | chardetng = "0.1.14" 14 | chrono = "0.4" 15 | bcrypt = "0.10" 16 | encoding_rs = "0.8.28" 17 | fs_extra = "1.2" 18 | include_dir = "0.6.2" 19 | jsonwebtoken = "7" 20 | lazy_static = "1.4.0" 21 | local-ip-address = "0.4.4" 22 | rand = "0.8.4" 23 | regex = "1.0" 24 | sha2 = "0.9.8" 25 | sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "sqlite" ] } 26 | sysinfo = "0.20" 27 | tokio = { version = "1.10.1", features = ["full"] } 28 | time = "0.2.11" 29 | urlencoding = "2.1.0" 30 | uuid = { version = "0.8", features = ["v4"] } 31 | walkdir = "2" 32 | -------------------------------------------------------------------------------- /backend/assets/migrations/20210829_init_db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS site ( 2 | site_id INTEGER PRIMARY KEY, 3 | name TEXT NOT NULL, 4 | version TEXT NOT NULL, 5 | storage TEXT, 6 | secret TEXT, 7 | language TEXT NOT NULL, 8 | update_freq TEXT NOT NULL, 9 | updated_at INTEGER, 10 | created_at INTEGER NOT NULL 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS user ( 14 | user_id INTEGER PRIMARY KEY, 15 | username TEXT NOT NULL UNIQUE, 16 | password TEXT NOT NULL, 17 | permission INTEGER NOT NULL DEFAULT 1, 18 | created_at INTEGER NOT NULL 19 | ); -------------------------------------------------------------------------------- /backend/assets/migrations/20211224_guest_login.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE site ADD allow_guest INTEGER NOT NULL DEFAULT 0; 2 | 3 | CREATE TABLE IF NOT EXISTS hidden ( 4 | hidden_id INTEGER PRIMARY KEY, 5 | path TEXT NOT NULL UNIQUE, 6 | least_permission INTEGER NOT NULL DEFAULT 0 7 | ); -------------------------------------------------------------------------------- /backend/assets/migrations/20220106_reset_password.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS reset ( 2 | reset_id TEXT PRIMARY KEY, 3 | reset_code TEXT NOT NULL, 4 | username TEXT NOT NULL UNIQUE, 5 | expire_at INTEGER NOT NULL 6 | ); -------------------------------------------------------------------------------- /backend/assets/oasis.conf.sample: -------------------------------------------------------------------------------- 1 | # Rename this file to `oasis.conf` to make it effective. 2 | 3 | # Uncomment following lines to config ip and port. 4 | # ip=0.0.0.0 5 | # port=8000 6 | 7 | # Uncomment following lines to config TLS certificate. 8 | # Could be relative or absolute path 9 | # certs = assets/tests/tls/rsa_sha256_cert.pem 10 | # key = /home/cheng/oasis/backend/assets/tests/tls/rsa_sha256_key.pem -------------------------------------------------------------------------------- /backend/assets/tests/01.srt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machengim/oasis/3555fe8581ded6e0206f2718e0517091135253ad/backend/assets/tests/01.srt -------------------------------------------------------------------------------- /backend/assets/tests/tls/rsa_sha256_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFLDCCAxSgAwIBAgIUNMz1ihOL/c1J1sgYy1c3ehB5z2cwDQYJKoZIhvcNAQEL 3 | BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg 4 | Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDcwOTIzMzMzM1oXDTMx 5 | MDcwNzIzMzMzM1owPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK 6 | DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQAD 7 | ggIPADCCAgoCggIBAMdnhZbeSDVddiKDOf+ZXjlChpgsiIizflHLHu4sWob3ZrwW 8 | BiICo2fezTZojcvFaMJooojlShENuLLxnPgc5O6WZiElR8vwAV/1ZJBzSdtplj/F 9 | NeT2AwR9Maw/XfQ7/Mg7z+svCrXwWq95RHs2Dd4Mci6WrQuDEs2rTlodNoA1KHu/ 10 | ll+YTkAn9rhSjkMu3hM2MLYO0dGhAKm2FTEmeYvmbo5ZIoiMcC7I5jJpWlnA/niE 11 | jCvku0CWXJSIrlFU36G0FzbkJOvnl/RlSr5jjEs8607Wf26wlCP6R880BzZKdNoi 12 | zzbc2Vj76kHuyOX2LAQ7v51p9n8PNuxnFJtJFnEPVXYlMenwgf63ElqQx8SoemlT 13 | ZT/yJv49qYHPXOEs8aoxeT9QhZ+3DtB43LOkxbUSsIQs7RNWTbZQIqi7eHi1PkFg 14 | yoHUKnWGLo1narDdlr0yvBz4FrnBTcb6JHCYK1dVm2+y7XspKDX4/8ymG3VOaPf6 15 | AoafSHoL/eF2sfK7DL4pTv5sDDQiBafL3+KWOMRD/UoVEvriPdgnuwf9sNGdM/rY 16 | 1vHjUgVnsD3UnkIoT7mhLKF+budS6KUaSh3ZA8+C8a82Zyeznxf9luR7hs7edt7G 17 | ehcyTJ3WfNOKslmXnvrwhJ7zHpd//TU9hkBJLyve1zNtZPsJH7N94wfUYd1nAgMB 18 | AAGjGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAgEA 19 | r7yYMGxmRTtDyN49y+pMbyiIjpKj8VW2TEfPFQvvOIRlGj6GT6EQMdeTigp9t+Sl 20 | v0lyMv6DjNPANzFAPvMdtQBU3SC1Grd1CtvRXqy290B2WV01Kpj4Yfi7+HMQgy6G 21 | FEytkm+FEUQjF2fwxsiMrSVeaVX7a67fSxXqzzANQToE994IdW/qQmlCuQvh0J8D 22 | N6IGY5N7tVIWDKJpH/YmKbe20dFdSk2NTHTZpLLm4Eqe/gG8qIUf6RBJgiqxJV0y 23 | YPKM3qXdwmwVFe/+EkX6BO3xqDmjqCi4eQRAbTDEmHqO4zVdXBQMkeRTWwH68r4D 24 | zRvZOA+ZDpnIMiSKozn6ZKM1py5m8BT2rD1ZRoMxNpaVauZzIu4V90oOB7Bii7hC 25 | HZDsHeX9kiGdeslSsyWYBEpeEeuf0MEz11pfROG2/zwk6StGPvK0xaKkRFifiTFq 26 | I1RSV0vG78zS73eTm5EABfAsAQTjQjkfnJEiTueMqoD8NMCgyogeuVr6p7DzDHgh 27 | 3VlzcImwOMSt1P1IRS5zty9AZR60Vrup33jYjCBj+GOQvU55etoHZV9PazEXbmN0 28 | v/zzIVvK3wF9NluX/ItSGTJkX+EDSCbFbp2U9C27XRHAIi+vRLYIZuWi3oLU8uLN 29 | bLlrSSTY/OmBxlmyayzPFElY5Qd8FHLQPF+his89eP4= 30 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /backend/assets/tests/tls/rsa_sha256_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDHZ4WW3kg1XXYi 3 | gzn/mV45QoaYLIiIs35Ryx7uLFqG92a8FgYiAqNn3s02aI3LxWjCaKKI5UoRDbiy 4 | 8Zz4HOTulmYhJUfL8AFf9WSQc0nbaZY/xTXk9gMEfTGsP130O/zIO8/rLwq18Fqv 5 | eUR7Ng3eDHIulq0LgxLNq05aHTaANSh7v5ZfmE5AJ/a4Uo5DLt4TNjC2DtHRoQCp 6 | thUxJnmL5m6OWSKIjHAuyOYyaVpZwP54hIwr5LtAllyUiK5RVN+htBc25CTr55f0 7 | ZUq+Y4xLPOtO1n9usJQj+kfPNAc2SnTaIs823NlY++pB7sjl9iwEO7+dafZ/Dzbs 8 | ZxSbSRZxD1V2JTHp8IH+txJakMfEqHppU2U/8ib+PamBz1zhLPGqMXk/UIWftw7Q 9 | eNyzpMW1ErCELO0TVk22UCKou3h4tT5BYMqB1Cp1hi6NZ2qw3Za9Mrwc+Ba5wU3G 10 | +iRwmCtXVZtvsu17KSg1+P/Mpht1Tmj3+gKGn0h6C/3hdrHyuwy+KU7+bAw0IgWn 11 | y9/iljjEQ/1KFRL64j3YJ7sH/bDRnTP62Nbx41IFZ7A91J5CKE+5oSyhfm7nUuil 12 | Gkod2QPPgvGvNmcns58X/Zbke4bO3nbexnoXMkyd1nzTirJZl5768ISe8x6Xf/01 13 | PYZASS8r3tczbWT7CR+zfeMH1GHdZwIDAQABAoICAQCEAEu2677xVMV3Y1dplKWD 14 | Uj63TgO0Ot5MVyJKmKH05qHjsNCugwCZKiy+78euNSh8SbgO13qIf4TdMISw2q/S 15 | IU3Kc1tr7Z17YH8KAMfLr8H+xRZAU3r75cSUOf6AR5W3F6E0FxgICOx/bM+goM/d 16 | Rm/v118GV+aCr/xWOzBw/r+l69YnwjNK1SnGKyBx6Pypyx3D51uOYf6GWjr9JnMf 17 | 4ZMeOHNb4VwCHIwGoydkcxYBwfzosaojv2XaDgEXZhAEg6s6cxzd7Znx93vbPRsK 18 | U3GR5vzE0a+/gVc4G0EK948TOCfkMZ7QATO6IdBsKuJIiyT1l8fNpMy/Ah3qDiAD 19 | 2+6PWUb7hnclFnIE0a4RYDNPUj5UEHT5emVwVU4/WOhDyUDMYP0hCXfBmRrsLu+C 20 | 4KQpx23CG8D5oDy+gkplvi+oYZwLbMFhRculepfZHiC7ySaLsyXjJ3IxwUVoZ4W3 21 | affFufroXn6+yNIneETUi+IAXuEKDujVahniKi0VWwQuP19yTybiLhNaJwmd3/mJ 22 | b8xtKUMPf5qQgJXrzugCo4ZpAd12nbVgztIkH2wkss61xQ1cReuAKMzaDxd44ndi 23 | h7EaXEiLxpNZlpKli4AviIutyoe/Z1rJL0LweeJv/Eos01f0snjcqJt7LTn9Jiiv 24 | ZbDltF1oR3HxLA7Ow8HGgQKCAQEA9Y6f60ngSlrgVGI/LTPLZUuEusu+hbNRoxgt 25 | 9TAuU8IRLxVkGNa8mXUuqbvxcmhEeXKtJVcLUHl75l2muVYoTgjxbufLeJnLtFdg 26 | AYYFLCF1k/xTIZBG67JdnLmhLUsDj9U0ugUOhtQ6qWbAq2O9TIfsC542Bg20T4km 27 | Lm6sNh717kwTTwiVgGG8koDmI8f8iFM9vITUWO6uwVc+VJS7jG9BL7TmRtUqCt5W 28 | 77pScWPPTPkvwjxVKqCay5HT6JXcZCBtizabrlB0e+/LisAfDLzUHstwRcsJ+zOb 29 | 59ZNpKVfUl36Wh6NJ7Y9r7Qgk0pPjl4xpoigjqtU0fEAkT+JKQKCAQEAz+Ju5HkG 30 | +SxKWUyTph824H6IaqJwAvybwTxvNzoH0yVTgzyADTBEjsvXbi/JREgXhPLQlp+/ 31 | LKlhWer4GcdxEQZZtYefIJ+LhKEPPlmgR/z8mR4sKKelB82Vt+qurKVTWXzfniBZ 32 | mgZj+E7KHLphkX1GRedy0s3jmbdcnA75Pevi7adgGVMzofAn3sKD63soQxyizmGJ 33 | Oqa6loywkMaiM0UlnrWcC0HnqJ2sodsVoJ66xRmdVZ6XIsahfHQrkvWXW1CDQPVN 34 | ej4RfGNI6cF558DNTB3PwzwiRBthqyaTD6nqwBBolYVzXsp2KD/dVmiIlHK1Xaqq 35 | OnoUAMZ8yni0DwKCAQEAuj6Z5pCa0GqK2RXHSxaMv2B+5FriP3AZjDUrrlsD2D1K 36 | YUa9K+W7GD17zfshjx+sR90FnFuf1kK+CaSgbtP9L+qyi+a9OdSUX00iISWwSJ98 37 | GWj4+G0AjYY0YEmfCMZrhi00l558PSE8+P1ZRuUYT7KMAufVm9PLHcQtNGx2q3ni 38 | GAKVZo1hLwVyTD/9zcfCLvfLzG+Gy4kE/NmaCfbhJQvBClkPi0vkXmfy0lKkcyI7 39 | uesKIS03f2Re4+XQLwlzJnI+A6fAfn7BSrs+yxcatcOGs3Cj0BvGj0O+jSHKtAVF 40 | /igPWUjw0Nz1fo2FY5GqM5YX3HKmLG+gnrdHMeNZuQKCAQAkOkSzAjhp4gMO7t5o 41 | O9ZXZxWk56v3iUgnc7259R35+O5F15xFMB0yeWmQpTlA8gNPQvWA2lP5l4cEoYMd 42 | EvmsStwFW54qlEM/GMZMSlg5U2g90tlFOHn1Eym9RGOuaJ1O4gkiSGb1BZoUYr6s 43 | JPrt3NQLSJtlC0ZXunGkLKPY26vPWLTRlQNRfEWmd2V/+xV4JJxmtO6yTu4DYH9A 44 | q60GnE1DDEkmWRTi+J9mEYUCWccYpC8cBag3AkCQLLqPQMdgvXYyMs2OuRRZBgBl 45 | 5Da3YY0lb6iOUIN0NQVfSzijqSvkzrc7H2eMpGHU/9Q1w7/Rhu/+Y8iIqk+kFvMW 46 | YdSXAoIBABYkzaflWSE2EnNDeMVLkQDHcC1zI08wfhAQH/oN832rqmCyL67GQU97 47 | mT3qOXG59RSODt2NcokEIuPo1CNlO9f9F/AXLel3eHErYrTIaYBVBTRI2O/1tkJi 48 | d0MY4CGw6qdqHguR3wx6b6iJwgRTrwwo6L+SOGmNmDlc84rt8f3SDVnsq+TLGeuL 49 | LwYN5uJn3fiiJXLNa6V5sbJHZ5xVgORpsM0LZDoLqZm2Dlw1JWKM44WBULu+G5LB 50 | hhqlyQOqwkGmAYmKWyZlOJtAWLbH6zrwcEo22EP0bQ2+NobkPPiNxv9o3PCWYE9p 51 | P8TQRNCBnG+P8u4TcF4+aFcjAlBHOUg= 52 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /backend/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | use rocket::Route; 2 | mod files; 3 | mod sys; 4 | mod upload; 5 | mod user; 6 | 7 | pub fn serve() -> Vec { 8 | let mut apis = vec![]; 9 | apis.append(&mut sys::route()); 10 | apis.append(&mut user::route()); 11 | apis.append(&mut files::route()); 12 | apis.append(&mut upload::route()); 13 | 14 | apis 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/entity/copy_move_task.rs: -------------------------------------------------------------------------------- 1 | use fs_extra::dir; 2 | use fs_extra::{copy_items_with_progress, TransitProcess}; 3 | use rocket::serde::{Deserialize, Serialize}; 4 | use std::path::PathBuf; 5 | 6 | use crate::COPY_MOVE_TASK; 7 | 8 | #[derive(Deserialize, Clone)] 9 | #[serde(crate = "rocket::serde")] 10 | pub struct CopyMoveFileRequest { 11 | pub source: String, 12 | pub target: String, 13 | pub is_copy: bool, 14 | pub overwrite: bool, 15 | } 16 | 17 | #[derive(Debug, Clone, PartialEq, Serialize)] 18 | #[serde(crate = "rocket::serde")] 19 | pub enum CopyMoveTaskStatus { 20 | Pending, 21 | InProgress, 22 | Success, 23 | Failed, 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize)] 27 | #[serde(crate = "rocket::serde")] 28 | pub struct CopyMoveTask { 29 | pub uuid: String, 30 | pub user_id: i64, 31 | pub status: CopyMoveTaskStatus, 32 | pub source: PathBuf, 33 | pub target: PathBuf, 34 | pub progress: f64, 35 | pub is_copy: bool, 36 | pub overwrite: bool, 37 | } 38 | 39 | impl CopyMoveTask { 40 | pub fn new( 41 | source: PathBuf, 42 | target: PathBuf, 43 | user_id: i64, 44 | is_copy: bool, 45 | overwrite: bool, 46 | ) -> Self { 47 | let uuid = uuid::Uuid::new_v4().to_string(); 48 | CopyMoveTask { 49 | uuid, 50 | source, 51 | target, 52 | progress: 0.0, 53 | user_id, 54 | is_copy, 55 | status: CopyMoveTaskStatus::Pending, 56 | overwrite, 57 | } 58 | } 59 | 60 | pub fn run(&self) { 61 | let task = self.clone(); 62 | std::thread::spawn(move || { 63 | let mut options = dir::CopyOptions::new(); 64 | if task.overwrite { 65 | options.overwrite = true; 66 | options.skip_exist = false; 67 | } else { 68 | options.skip_exist = true; 69 | } 70 | 71 | let handle = |info: TransitProcess| { 72 | task.update_progress( 73 | info.copied_bytes as f64 / info.total_bytes as f64, 74 | CopyMoveTaskStatus::InProgress, 75 | ); 76 | dir::TransitProcessResult::ContinueOrAbort 77 | }; 78 | 79 | let from_paths = vec![&task.source]; 80 | if let Err(e) = copy_items_with_progress(&from_paths, &task.target, &options, handle) { 81 | eprintln!("Error: {}", e); 82 | task.update_progress(0.0, CopyMoveTaskStatus::Failed); 83 | } 84 | 85 | if !task.is_copy { 86 | if let Err(e) = fs_extra::remove_items(&vec![&task.source]) { 87 | eprintln!("Error: {}", e); 88 | task.update_progress(0.0, CopyMoveTaskStatus::Failed); 89 | return; 90 | } 91 | } 92 | 93 | task.update_progress(1.0, CopyMoveTaskStatus::Success); 94 | }); 95 | } 96 | 97 | pub fn update_progress(&self, progress: f64, status: CopyMoveTaskStatus) { 98 | let mut updated_task = self.clone(); 99 | updated_task.progress = progress; 100 | updated_task.status = status; 101 | updated_task.set_static_value(); 102 | } 103 | 104 | pub fn set_static_value(&self) { 105 | let mut copy_move_task = COPY_MOVE_TASK.lock().unwrap(); 106 | *copy_move_task = Some(self.clone()); 107 | } 108 | 109 | pub fn get_static_value() -> Option { 110 | let copy_move_task = COPY_MOVE_TASK.lock().unwrap(); 111 | copy_move_task.clone() 112 | } 113 | 114 | pub fn allow_new_task() -> bool { 115 | let task = Self::get_static_value(); 116 | if task.is_none() { 117 | return true; 118 | } 119 | 120 | let task = task.as_ref().unwrap(); 121 | task.status == CopyMoveTaskStatus::Success || task.status == CopyMoveTaskStatus::Failed 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /backend/src/entity/error.rs: -------------------------------------------------------------------------------- 1 | use rocket::http::Status; 2 | use rocket::request::Request; 3 | use rocket::response::{self, Responder}; 4 | use std::error::Error as StdError; 5 | use std::fmt; 6 | 7 | #[derive(Debug)] 8 | pub enum Error { 9 | BadRequest, 10 | Conflict, 11 | Forbidden, 12 | InternalServerError, 13 | NotFound, 14 | Unauthorized, 15 | } 16 | 17 | impl fmt::Display for Error { 18 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 19 | match *self { 20 | Error::BadRequest => f.write_str("BadRequest"), 21 | Error::Conflict => f.write_str("Conflict"), 22 | Error::Forbidden => f.write_str("Forbidden"), 23 | Error::InternalServerError => f.write_str("InternalServerError"), 24 | Error::NotFound => f.write_str("NotFound"), 25 | Error::Unauthorized => f.write_str("Unauthorized"), 26 | } 27 | } 28 | } 29 | 30 | impl StdError for Error { 31 | fn description(&self) -> &str { 32 | match *self { 33 | Error::BadRequest => "BadRequest", 34 | Error::Conflict => "Conflict", 35 | Error::Forbidden => "Forbidden", 36 | Error::InternalServerError => "InternalServerError", 37 | Error::NotFound => "NotFound", 38 | Error::Unauthorized => "Unauthorized", 39 | } 40 | } 41 | } 42 | 43 | impl<'r> Responder<'r, 'static> for Error { 44 | fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { 45 | match self { 46 | Error::BadRequest => Err(Status::BadRequest), 47 | Error::Conflict => Err(Status::Conflict), 48 | Error::Forbidden => Err(Status::Forbidden), 49 | Error::InternalServerError => Err(Status::InternalServerError), 50 | Error::NotFound => Err(Status::NotFound), 51 | Error::Unauthorized => Err(Status::Unauthorized), 52 | } 53 | } 54 | } 55 | 56 | impl From for Error { 57 | fn from(_: std::io::Error) -> Self { 58 | Error::InternalServerError 59 | } 60 | } 61 | 62 | impl From for Error { 63 | fn from(_: sqlx::Error) -> Self { 64 | Error::InternalServerError 65 | } 66 | } 67 | 68 | impl From for Error { 69 | fn from(_: anyhow::Error) -> Self { 70 | Error::InternalServerError 71 | } 72 | } 73 | 74 | impl From for Error { 75 | fn from(number: i32) -> Self { 76 | match number { 77 | 400 => Error::BadRequest, 78 | 409 => Error::Conflict, 79 | 401 => Error::Unauthorized, 80 | 403 => Error::Forbidden, 81 | 404 => Error::NotFound, 82 | _ => Error::InternalServerError, 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /backend/src/entity/file.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result as AnyResult; 2 | use rocket::serde::Serialize; 3 | use std::path::PathBuf; 4 | 5 | #[derive(Serialize)] 6 | #[serde(crate = "rocket::serde")] 7 | pub struct File { 8 | pub dir: Option, 9 | pub filename: String, 10 | pub file_type: FileType, 11 | pub size: u64, 12 | pub least_permission: i8, 13 | } 14 | 15 | #[derive(Serialize, PartialEq)] 16 | #[serde(crate = "rocket::serde")] 17 | pub enum FileType { 18 | Dir, 19 | Code, 20 | Text, 21 | Image, 22 | Music, 23 | Video, 24 | Pdf, 25 | Unknown, 26 | } 27 | 28 | impl FileType { 29 | fn infer_file_type(ext: &str) -> Self { 30 | match ext.to_lowercase().as_str() { 31 | "c" | "cpp" | "js" | "ts" | "rs" | "py" | "java" | "html" | "css" | "sh" => Self::Code, 32 | "jpg" | "jpeg" | "gif" | "png" => Self::Image, 33 | "mp3" | "flac" | "aac" | "ogg" | "wav" => Self::Music, 34 | "pdf" => Self::Pdf, 35 | "mp4" | "mov" | "avi" | "mkv" | "webm" | "flv" | "wmv" => Self::Video, 36 | "txt" | "md" | "srt" | "vtt" | "json" | "yml" | "ini" | "conf" => Self::Text, 37 | _ => Self::Unknown, 38 | } 39 | } 40 | 41 | pub fn get_file_type(path: &PathBuf) -> Self { 42 | match (path.is_dir(), path.extension()) { 43 | (true, _) => Self::Dir, 44 | (false, Some(ext)) => Self::infer_file_type(ext.to_str().unwrap_or("")), 45 | (false, None) => Self::Unknown, 46 | } 47 | } 48 | } 49 | 50 | impl File { 51 | pub fn from_path( 52 | path: &PathBuf, 53 | need_dir: bool, 54 | storage: &str, 55 | least_permission: i8, 56 | ) -> AnyResult { 57 | let filename = match path.file_name() { 58 | Some(str) => str.to_string_lossy().to_string(), 59 | None => { 60 | return Err(anyhow::anyhow!("Cannot get filename")); 61 | } 62 | }; 63 | 64 | let file_type = FileType::get_file_type(path); 65 | let size = match (path.is_dir(), path.metadata()) { 66 | (false, Ok(meta)) => meta.len(), 67 | _ => 0, 68 | }; 69 | 70 | let dir = if need_dir { 71 | let parent_dir = path.parent().unwrap().strip_prefix(storage)?; 72 | Some(PathBuf::from(parent_dir)) 73 | } else { 74 | None 75 | }; 76 | 77 | Ok(Self { 78 | dir, 79 | filename, 80 | file_type, 81 | size, 82 | least_permission, 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /backend/src/entity/hidden.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | args, 3 | util::db::{self, Query}, 4 | }; 5 | use anyhow::Result as AnyResult; 6 | use rocket::serde::Serialize; 7 | use sqlx::{pool::PoolConnection, FromRow, Sqlite, Transaction}; 8 | 9 | #[derive(Serialize, FromRow, Debug)] 10 | #[serde(crate = "rocket::serde")] 11 | pub struct Hidden { 12 | pub hidden_id: i64, 13 | pub path: String, 14 | pub least_permission: i8, 15 | } 16 | 17 | impl Hidden { 18 | pub fn new(path: &str, least_permission: i8) -> Self { 19 | Self { 20 | hidden_id: 0, 21 | path: String::from(path), 22 | least_permission, 23 | } 24 | } 25 | 26 | pub async fn insert_query(&self, tx: &mut Transaction<'_, Sqlite>) -> AnyResult { 27 | let sql = "insert into HIDDEN (path, least_permission) values (?1, ?2)"; 28 | let query = Query::new(sql, args![&self.path, self.least_permission]); 29 | 30 | let uid = db::execute(query, tx).await?; 31 | Ok(uid) 32 | } 33 | 34 | pub async fn delete_query(path: &str, tx: &mut Transaction<'_, Sqlite>) -> AnyResult<()> { 35 | let sql = "delete from HIDDEN where path = ?1"; 36 | let query = Query::new(sql, args![path]); 37 | 38 | db::execute(query, tx).await?; 39 | Ok(()) 40 | } 41 | 42 | pub async fn delete_all_query(tx: &mut Transaction<'_, Sqlite>) -> AnyResult<()> { 43 | let sql = "delete from HIDDEN"; 44 | let query = Query { 45 | sql, 46 | args: Vec::new(), 47 | }; 48 | 49 | db::execute(query, tx).await?; 50 | Ok(()) 51 | } 52 | 53 | // Update the record with the exact path 54 | // Then update all records (child files) start with `path + "/"` 55 | pub async fn update_all_sub_path_query( 56 | tx: &mut Transaction<'_, Sqlite>, 57 | current_path: &str, 58 | new_path: &str, 59 | ) -> AnyResult<()> { 60 | let sql = "update HIDDEN set path = ?1 where path = ?2"; 61 | let query = Query::new(sql, args![current_path, new_path]); 62 | 63 | db::execute(query, tx).await?; 64 | 65 | // update hidden set path = replace(substr(path, 1, 7), 'alpine/', 'alpine1/') || substr(path, 8) where path like "alpine/%"; 66 | let sql = 67 | "update HIDDEN set path = replace(substr(path, ?1, ?2), ?3, ?4) || substr(path, ?5) where path like ?6"; 68 | let query = Query::new( 69 | sql, 70 | args![ 71 | 1, 72 | current_path.len() + 2, 73 | format!("{}/", current_path), 74 | format!("{}/", new_path), 75 | current_path.len() + 3, 76 | format!("{}/%", current_path) 77 | ], 78 | ); 79 | 80 | db::execute(query, tx).await?; 81 | 82 | Ok(()) 83 | } 84 | 85 | pub async fn delete_all_sub_path_query( 86 | tx: &mut Transaction<'_, Sqlite>, 87 | path: &str, 88 | ) -> AnyResult<()> { 89 | let sql = "delete from HIDDEN where path = ?1"; 90 | let query = Query::new(sql, args![path]); 91 | db::execute(query, tx).await?; 92 | 93 | let sql = "delete from HIDDEN where path like ?1"; 94 | let query = Query::new(sql, args![format!("{}/%", path)]); 95 | db::execute(query, tx).await?; 96 | 97 | Ok(()) 98 | } 99 | 100 | pub async fn find_all(conn: &mut PoolConnection) -> AnyResult> { 101 | let sql = "select * from HIDDEN"; 102 | let query = Query { 103 | sql, 104 | args: Vec::new(), 105 | }; 106 | 107 | let hiddens = db::fetch_multiple::(query, conn).await?; 108 | Ok(hiddens) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /backend/src/entity/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod copy_move_task; 2 | pub mod error; 3 | pub mod file; 4 | pub mod hidden; 5 | pub mod request; 6 | pub mod reset_password; 7 | pub mod response; 8 | pub mod site; 9 | pub mod upload_task; 10 | pub mod user; 11 | -------------------------------------------------------------------------------- /backend/src/entity/request.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::Deserialize; 2 | 3 | #[derive(Deserialize, Debug)] 4 | #[serde(crate = "rocket::serde")] 5 | pub struct GenerateLinkRequest { 6 | pub path: String, 7 | pub expire: i64, 8 | } 9 | 10 | #[derive(Deserialize, Debug)] 11 | #[serde(crate = "rocket::serde")] 12 | pub struct SetupRequest { 13 | pub sitename: String, 14 | pub username: String, 15 | pub password: String, 16 | pub storage: String, 17 | pub language: String, 18 | } 19 | 20 | #[derive(Deserialize, Debug)] 21 | #[serde(crate = "rocket::serde")] 22 | pub struct UpdateSiteRequest { 23 | pub sitename: String, 24 | pub storage: String, 25 | pub language: String, 26 | pub update_freq: String, 27 | pub allow_guest: bool, 28 | } 29 | 30 | #[derive(Deserialize, Debug)] 31 | #[serde(crate = "rocket::serde")] 32 | pub struct LoginRequest { 33 | pub username: String, 34 | pub password: String, 35 | } 36 | 37 | #[derive(Deserialize)] 38 | #[serde(crate = "rocket::serde")] 39 | pub struct ChangePasswordRequest { 40 | pub username: String, 41 | pub old_password: String, 42 | pub new_password: String, 43 | } 44 | 45 | #[derive(Deserialize, Debug)] 46 | #[serde(crate = "rocket::serde")] 47 | pub struct UploadRequest { 48 | pub filename: String, 49 | pub size: u64, 50 | pub target: String, 51 | pub hash: String, 52 | } 53 | 54 | #[derive(Deserialize, Debug)] 55 | #[serde(crate = "rocket::serde")] 56 | pub struct UploadSliceRequest { 57 | pub hash: String, 58 | pub index: u64, 59 | pub data: Vec, 60 | } 61 | 62 | #[derive(Deserialize, Debug)] 63 | #[serde(crate = "rocket::serde")] 64 | pub struct CancelUploadRequest { 65 | pub uuids: Vec, 66 | } 67 | 68 | #[derive(Deserialize, Debug)] 69 | #[serde(crate = "rocket::serde")] 70 | pub struct CreateDirRequest { 71 | pub parent: String, 72 | pub name: String, 73 | } 74 | 75 | #[derive(Deserialize, Debug)] 76 | #[serde(crate = "rocket::serde")] 77 | pub struct RenameFileRequest { 78 | pub new_name: String, 79 | } 80 | 81 | #[derive(Deserialize)] 82 | #[serde(crate = "rocket::serde")] 83 | pub struct SetFileVisibilityRequest { 84 | pub visible: bool, 85 | } 86 | 87 | #[derive(Deserialize)] 88 | #[serde(crate = "rocket::serde")] 89 | pub struct ForgotPasswordRequest { 90 | pub url: String, 91 | pub username: String, 92 | } 93 | 94 | #[derive(Deserialize)] 95 | #[serde(crate = "rocket::serde")] 96 | pub struct ResetPasswordRequest { 97 | pub uuid: String, 98 | pub code: String, 99 | pub username: String, 100 | pub password: String, 101 | } 102 | -------------------------------------------------------------------------------- /backend/src/entity/reset_password.rs: -------------------------------------------------------------------------------- 1 | use crate::util::db::{self, Query}; 2 | use crate::{args, util}; 3 | use anyhow::Result as AnyResult; 4 | use rocket::serde::Serialize; 5 | use sqlx::pool::PoolConnection; 6 | use sqlx::{FromRow, Sqlite, Transaction}; 7 | use tokio::fs; 8 | 9 | #[derive(Serialize, FromRow, Debug)] 10 | #[serde(crate = "rocket::serde")] 11 | pub struct ResetPassword { 12 | pub reset_id: String, 13 | pub reset_code: String, 14 | pub username: String, 15 | pub expire_at: i64, 16 | } 17 | 18 | impl ResetPassword { 19 | pub fn new(username: &str) -> Self { 20 | let uuid = uuid::Uuid::new_v4().to_string(); 21 | let expire_at = util::get_utc_seconds() + 60 * 60 * 6; 22 | let reset_code = util::generate_secret_key(6); 23 | 24 | Self { 25 | reset_id: uuid, 26 | reset_code, 27 | username: username.to_string(), 28 | expire_at, 29 | } 30 | } 31 | 32 | pub async fn insert_query(&self, tx: &mut Transaction<'_, Sqlite>) -> AnyResult { 33 | // One user can only have one reset password record. 34 | Self::delete_query(&self.username, tx).await?; 35 | 36 | let sql = 37 | "insert into RESET (reset_id, username, expire_at, reset_code) values (?1, ?2, ?3, ?4)"; 38 | let query = Query::new( 39 | sql, 40 | args![ 41 | &self.reset_id, 42 | &self.username, 43 | self.expire_at, 44 | &self.reset_code 45 | ], 46 | ); 47 | 48 | let uid = db::execute(query, tx).await?; 49 | Ok(uid) 50 | } 51 | 52 | pub async fn delete_query(username: &str, tx: &mut Transaction<'_, Sqlite>) -> AnyResult<()> { 53 | let sql = "delete from RESET where username = ?1"; 54 | let query = Query::new(sql, args![username]); 55 | 56 | db::execute(query, tx).await?; 57 | Ok(()) 58 | } 59 | 60 | pub async fn remove_user_reset_password_files( 61 | &self, 62 | conn: &mut PoolConnection, 63 | ) -> AnyResult<()> { 64 | let sql = "select * from RESET where username = ?1"; 65 | let query = Query::new(sql, args![self.username]); 66 | let resets: Vec = db::fetch_multiple(query, conn).await?; 67 | let temp_path = util::get_data_temp_path(); 68 | 69 | for reset in resets { 70 | let file_path = temp_path.join(format!("{}.txt", reset.reset_id)); 71 | if file_path.exists() { 72 | fs::remove_file(&file_path).await?; 73 | } 74 | } 75 | 76 | Ok(()) 77 | } 78 | 79 | pub async fn write_reset_password_file(&self, url: &str) -> AnyResult<()> { 80 | let temp_path = util::get_data_temp_path(); 81 | if !temp_path.exists() { 82 | fs::create_dir_all(&temp_path).await?; 83 | } 84 | 85 | let file_name = format!("{}.txt", self.reset_id); 86 | let file_path = temp_path.join(&file_name); 87 | let content = 88 | format!("Dear {}:\n\nPlease visit the following link to reset your password before it expires:\n\n{}/reset-password/{}\n\nYour reset code is: {}\n\nPlease keep this file confidential.", self.username, url, self.reset_id, self.reset_code); 89 | util::file_system::write_text_file(&file_path, &content).await?; 90 | 91 | Ok(()) 92 | } 93 | 94 | pub async fn from_reset_req( 95 | uuid: &str, 96 | username: &str, 97 | code: &str, 98 | conn: &mut PoolConnection, 99 | ) -> AnyResult { 100 | let sql = "select * from RESET where reset_id = ?1 and username = ?2 and reset_code = ?3"; 101 | let query = Query::new(sql, args![uuid, username, code]); 102 | let reset: ResetPassword = match db::fetch_single(query, conn).await? { 103 | Some(reset) => reset, 104 | None => return Err(anyhow::anyhow!("Reset password request not found")), 105 | }; 106 | 107 | if reset.expire_at < util::get_utc_seconds() { 108 | return Err(anyhow::anyhow!("Reset password request expired")); 109 | } 110 | 111 | let reset_pw = Self { 112 | reset_id: uuid.to_string(), 113 | reset_code: code.to_string(), 114 | username: username.to_string(), 115 | expire_at: reset.expire_at, 116 | }; 117 | 118 | Ok(reset_pw) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /backend/src/entity/response.rs: -------------------------------------------------------------------------------- 1 | use crate::service::range::RangedFile; 2 | use crate::util::constants::{DEFAULT_APP_NAME, DEFAULT_LANGUAGE, DEFAULT_UPDATE_FREQ, VERSION}; 3 | use rocket::fs::NamedFile; 4 | use rocket::serde::Serialize; 5 | 6 | use super::site::Site; 7 | 8 | #[derive(Responder)] 9 | pub enum FileResponse { 10 | Range(RangedFile), 11 | Binary(NamedFile), 12 | Text(String), 13 | } 14 | 15 | #[derive(Serialize)] 16 | #[serde(crate = "rocket::serde")] 17 | pub struct AppNeedUpdateResponse { 18 | pub need: bool, 19 | pub url: String, 20 | } 21 | 22 | #[derive(Serialize)] 23 | #[serde(crate = "rocket::serde")] 24 | pub struct LoginResponse { 25 | pub username: String, 26 | pub permission: i8, 27 | pub expire: usize, 28 | } 29 | 30 | #[derive(Serialize, Debug)] 31 | #[serde(crate = "rocket::serde")] 32 | pub struct SiteBriefResponse { 33 | pub name: String, 34 | pub version: String, 35 | pub language: String, 36 | pub update_freq: String, 37 | pub allow_guest: bool, 38 | } 39 | 40 | #[derive(Serialize, Debug)] 41 | #[serde(crate = "rocket::serde")] 42 | pub struct SiteFullResponse { 43 | pub name: String, 44 | pub version: String, 45 | pub language: String, 46 | pub update_freq: String, 47 | pub storage: String, 48 | pub allow_guest: bool, 49 | } 50 | 51 | impl From for SiteBriefResponse { 52 | fn from(s: Site) -> Self { 53 | Self { 54 | name: s.name, 55 | version: s.version, 56 | language: s.language, 57 | update_freq: s.update_freq, 58 | allow_guest: s.allow_guest > 0, 59 | } 60 | } 61 | } 62 | 63 | impl Default for SiteBriefResponse { 64 | fn default() -> Self { 65 | Self { 66 | name: DEFAULT_APP_NAME.to_owned(), 67 | version: VERSION.to_owned(), 68 | language: DEFAULT_LANGUAGE.to_owned(), 69 | update_freq: DEFAULT_UPDATE_FREQ.to_owned(), 70 | allow_guest: false, 71 | } 72 | } 73 | } 74 | 75 | impl From for SiteFullResponse { 76 | fn from(s: Site) -> Self { 77 | Self { 78 | name: s.name, 79 | version: s.version, 80 | language: s.language, 81 | storage: s.storage, 82 | update_freq: s.update_freq, 83 | allow_guest: s.allow_guest > 0, 84 | } 85 | } 86 | } 87 | 88 | impl Default for SiteFullResponse { 89 | fn default() -> Self { 90 | Self { 91 | name: DEFAULT_APP_NAME.to_owned(), 92 | version: VERSION.to_owned(), 93 | language: DEFAULT_LANGUAGE.to_owned(), 94 | storage: String::new(), 95 | update_freq: DEFAULT_UPDATE_FREQ.to_owned(), 96 | allow_guest: false, 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /backend/src/entity/site.rs: -------------------------------------------------------------------------------- 1 | use crate::args; 2 | use crate::util::{ 3 | self, 4 | constants::DEFAULT_UPDATE_FREQ, 5 | db::{self, fetch_single, Query}, 6 | }; 7 | use anyhow::Result as AnyResult; 8 | use sqlx::pool::PoolConnection; 9 | use sqlx::{FromRow, Sqlite, Transaction}; 10 | use std::path::PathBuf; 11 | 12 | #[derive(FromRow, Default, Debug)] 13 | pub struct Site { 14 | pub site_id: i64, 15 | pub name: String, 16 | pub version: String, 17 | pub storage: String, 18 | pub secret: String, 19 | pub language: String, 20 | pub update_freq: String, 21 | pub created_at: i64, 22 | pub updated_at: i64, 23 | pub allow_guest: i8, 24 | } 25 | 26 | impl Site { 27 | pub fn new(name: &str, storage: &PathBuf, language: &str, created_at: i64) -> Self { 28 | let secret = util::generate_secret_key(32); 29 | let version = util::get_version_constant(); 30 | let storage_str = storage.to_str().unwrap().to_owned(); 31 | let update_freq = DEFAULT_UPDATE_FREQ.to_owned(); 32 | 33 | Self { 34 | name: name.to_owned(), 35 | site_id: 0, 36 | version, 37 | storage: storage_str, 38 | secret, 39 | language: language.to_owned(), 40 | update_freq, 41 | created_at, 42 | updated_at: created_at, 43 | allow_guest: 0, 44 | } 45 | } 46 | 47 | pub async fn read(conn: &mut PoolConnection) -> AnyResult> { 48 | let sql = "select * from Site"; 49 | let query = Query::new(sql, vec![]); 50 | 51 | Ok(fetch_single(query, conn).await?) 52 | } 53 | 54 | // Allow guest is set to 0 when initilizing. 55 | pub async fn insert(&self, tx: &mut Transaction<'_, Sqlite>) -> anyhow::Result { 56 | let sql = "insert into SITE (name, version, storage, secret, created_at, language, update_freq, updated_at) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"; 57 | let query = Query::new( 58 | sql, 59 | args![ 60 | &self.name, 61 | self.version, 62 | &self.storage, 63 | &self.secret, 64 | self.created_at, 65 | &self.language, 66 | &self.update_freq, 67 | &self.updated_at 68 | ], 69 | ); 70 | 71 | let site_id = db::execute(query, tx).await?; 72 | 73 | Ok(site_id) 74 | } 75 | 76 | pub async fn update(&self, tx: &mut Transaction<'_, Sqlite>) -> AnyResult { 77 | let sql = "update SITE set name = ?1, version = ?2, storage = ?3, secret = ?4, created_at = ?5, language = ?6, update_freq = ?7, updated_at = ?8, allow_guest = ?9"; 78 | let query = Query::new( 79 | sql, 80 | args![ 81 | &self.name, 82 | self.version, 83 | &self.storage, 84 | &self.secret, 85 | self.created_at, 86 | &self.language, 87 | &self.update_freq, 88 | &self.updated_at, 89 | &self.allow_guest 90 | ], 91 | ); 92 | 93 | Ok(db::execute(query, tx).await?) 94 | } 95 | 96 | pub fn check_update_need(&self) -> bool { 97 | let current_timestamp = util::get_utc_seconds(); 98 | let seconds_per_day = 24 * 60 * 60; 99 | let interval = match self.update_freq.to_lowercase().as_str() { 100 | "daily" => seconds_per_day, 101 | "weekly" => 7 * seconds_per_day, 102 | "monthly" => 30 * seconds_per_day, 103 | _ => return false, 104 | }; 105 | 106 | current_timestamp - self.updated_at > interval 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /backend/src/entity/upload_task.rs: -------------------------------------------------------------------------------- 1 | use super::request::UploadRequest; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct UploadTask { 6 | pub uuid: String, 7 | pub userid: i64, 8 | pub filename: String, 9 | pub size: u64, 10 | pub dir: PathBuf, 11 | pub hash: String, 12 | pub finished_slices: u64, 13 | } 14 | 15 | // Not implementing request guard as the different verification logic 16 | // for uploading and cancelling upload. The second endpoint requires 17 | // a list of task uuids. 18 | impl UploadTask { 19 | pub fn new(upload_req: &UploadRequest, userid: i64, target_path: PathBuf) -> Self { 20 | let uuid = uuid::Uuid::new_v4().to_string(); 21 | 22 | Self { 23 | uuid, 24 | userid, 25 | filename: upload_req.filename.to_owned(), 26 | size: upload_req.size, 27 | dir: target_path, 28 | hash: upload_req.hash.to_owned(), 29 | finished_slices: 0, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/entity/user.rs: -------------------------------------------------------------------------------- 1 | use super::request::SetupRequest; 2 | use crate::{ 3 | args, 4 | service::token::{AccessToken, RefreshToken}, 5 | util::db, 6 | util::db::Query, 7 | }; 8 | use anyhow::Result as AnyResult; 9 | use bcrypt::{hash, verify, DEFAULT_COST}; 10 | use sqlx::{pool::PoolConnection, FromRow, Sqlite, Transaction}; 11 | 12 | #[derive(FromRow)] 13 | pub struct User { 14 | pub user_id: i64, 15 | pub username: String, 16 | pub password: String, 17 | pub permission: i8, 18 | pub created_at: i64, 19 | } 20 | 21 | impl User { 22 | pub fn from_setup_req(req: &SetupRequest, created_at: i64) -> Self { 23 | Self { 24 | user_id: 0, 25 | username: req.username.to_string(), 26 | password: req.password.to_string(), 27 | permission: 9, 28 | created_at, 29 | } 30 | } 31 | 32 | pub fn guest(created_at: i64) -> Self { 33 | Self { 34 | user_id: 0, 35 | username: String::from("Guest"), 36 | password: String::new(), 37 | permission: 0, 38 | created_at, 39 | } 40 | } 41 | 42 | pub async fn insert_query(&self, tx: &mut Transaction<'_, Sqlite>) -> AnyResult { 43 | let encrypt_password = hash(&self.password, DEFAULT_COST)?; 44 | 45 | let sql = 46 | "insert into USER (username, password, permission, created_at) values(?1, ?2, ?3, ?4)"; 47 | let query = Query::new( 48 | sql, 49 | args![ 50 | &self.username, 51 | &encrypt_password, 52 | self.permission, 53 | self.created_at 54 | ], 55 | ); 56 | 57 | let uid = db::execute(query, tx).await?; 58 | 59 | Ok(uid) 60 | } 61 | 62 | pub async fn update(&self, tx: &mut Transaction<'_, Sqlite>) -> AnyResult { 63 | let encrypt_password = hash(&self.password, DEFAULT_COST)?; 64 | 65 | let sql = 66 | "update USER set username = ?1, password = ?2, permission = ?3, created_at = ?4 where user_id = ?5"; 67 | let query = Query::new( 68 | sql, 69 | args![ 70 | &self.username, 71 | &encrypt_password, 72 | self.permission, 73 | self.created_at, 74 | self.user_id 75 | ], 76 | ); 77 | 78 | let uid = db::execute(query, tx).await?; 79 | 80 | Ok(uid) 81 | } 82 | 83 | pub async fn find_user_by_name( 84 | username: &str, 85 | conn: &mut PoolConnection, 86 | ) -> AnyResult> { 87 | let sql = "select * from USER where username = ?1"; 88 | let query = Query::new(sql, args![username]); 89 | 90 | Ok(db::fetch_single(query, conn).await?) 91 | } 92 | 93 | pub async fn find_user_by_id( 94 | uid: i64, 95 | conn: &mut PoolConnection, 96 | ) -> AnyResult> { 97 | let sql = "select * from USER where user_id = ?1"; 98 | let query = Query::new(sql, args![uid]); 99 | 100 | Ok(db::fetch_single(query, conn).await?) 101 | } 102 | 103 | pub async fn login( 104 | username: &str, 105 | password: &str, 106 | conn: &mut PoolConnection, 107 | ) -> AnyResult { 108 | let user = match Self::find_user_by_name(username, conn).await? { 109 | Some(u) => u, 110 | _ => return Err(anyhow::anyhow!("No such username in db")), 111 | }; 112 | 113 | if !verify(password, &user.password)? { 114 | return Err(anyhow::anyhow!("Invalid password to login")); 115 | } 116 | 117 | Ok(user) 118 | } 119 | 120 | pub fn generate_access_token(&self) -> AccessToken { 121 | AccessToken::new(self.user_id, self.permission) 122 | } 123 | 124 | pub fn generate_refresh_token(&self) -> RefreshToken { 125 | RefreshToken::new(self.user_id) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /backend/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rocket; 3 | mod api; 4 | mod entity; 5 | mod service; 6 | mod util; 7 | use crate::util::local_ip::ServerConfig; 8 | use entity::{copy_move_task::CopyMoveTask, site::Site}; 9 | use lazy_static::lazy_static; 10 | use rocket::fs::FileServer; 11 | use service::app_state::AppState; 12 | use service::fairings::StaticFileCache; 13 | use std::sync::Mutex; 14 | use std::{sync::Arc, thread, time}; 15 | use util::{init, local_ip, rocket_env::RocketEnv}; 16 | 17 | lazy_static! { 18 | static ref COPY_MOVE_TASK: Arc>> = Arc::new(Mutex::new(None)); 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() { 23 | if let Err(e) = launch().await { 24 | eprintln!("{}", e); 25 | 26 | // For windows users, persist the window for 5 seconds to read the error message. 27 | if std::env::consts::OS == "windows" { 28 | let five_seconds = time::Duration::from_secs(5); 29 | thread::sleep(five_seconds); 30 | } 31 | } 32 | 33 | println!("Server shutdown"); 34 | } 35 | 36 | async fn launch() -> Result<(), anyhow::Error> { 37 | init::init_app().await?; 38 | let pool = init::get_db_pool().await?; 39 | let mut conn = pool.acquire().await?; 40 | init::check_update(&mut conn).await?; 41 | 42 | let site_op = Site::read(&mut conn).await?; 43 | let state = AppState::new(site_op, pool); 44 | let config = ServerConfig::new()?; 45 | RocketEnv::setup(&config); 46 | 47 | let rocket = rocket::build() 48 | .manage(state) 49 | .attach(StaticFileCache) 50 | .mount("/api", api::serve()) 51 | .mount("/", service::static_route::serve()) 52 | .mount("/", FileServer::from(util::get_frontend_path())) 53 | .ignite() 54 | .await?; 55 | 56 | local_ip::show(&config)?; 57 | rocket.launch().await?; 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/service/app_state.rs: -------------------------------------------------------------------------------- 1 | use crate::entity::{site::Site, upload_task::UploadTask}; 2 | use anyhow::Result as AnyResult; 3 | use sqlx::{pool::PoolConnection, Pool, Sqlite}; 4 | use std::sync::{atomic::AtomicBool, atomic::Ordering, Arc, Mutex, MutexGuard}; 5 | 6 | #[derive(Debug)] 7 | pub struct AppState { 8 | pub first_run: AtomicBool, 9 | pub site: Arc>, 10 | pub pool: Pool, 11 | pub uploads: Arc>>, 12 | } 13 | 14 | impl AppState { 15 | pub fn new(site_op: Option, pool: Pool) -> Self { 16 | let first_run = site_op.is_none(); 17 | let site = match site_op { 18 | Some(site) => site, 19 | None => Site::default(), 20 | }; 21 | 22 | Self { 23 | first_run: AtomicBool::new(first_run), 24 | site: Arc::new(Mutex::new(site)), 25 | pool, 26 | uploads: Arc::new(Mutex::new(vec![])), 27 | } 28 | } 29 | 30 | pub fn get_first_run(&self) -> bool { 31 | self.first_run.load(Ordering::Relaxed) 32 | } 33 | 34 | pub fn set_first_run(&self, new_first_run: bool) { 35 | self.first_run.store(new_first_run, Ordering::Relaxed); 36 | } 37 | 38 | pub fn get_secret(&self) -> AnyResult { 39 | Ok(self.get_site()?.secret.to_owned()) 40 | } 41 | 42 | pub fn get_allow_guest(&self) -> AnyResult { 43 | Ok(self.get_site()?.allow_guest) 44 | } 45 | 46 | pub async fn get_pool_conn(&self) -> Result, sqlx::Error> { 47 | Ok(self.pool.acquire().await?) 48 | } 49 | 50 | pub fn get_site(&self) -> AnyResult> { 51 | match self.site.lock() { 52 | Ok(v) => Ok(v), 53 | Err(e) => return Err(anyhow::anyhow!("Cannot retrieve site from state: {}", e)), 54 | } 55 | } 56 | 57 | pub fn set_site(&self, new_site: Site) -> AnyResult<()> { 58 | let mut site = self.get_site()?; 59 | *site = new_site; 60 | 61 | Ok(()) 62 | } 63 | 64 | pub fn get_upload_tasks(&self) -> AnyResult>> { 65 | match self.uploads.lock() { 66 | Ok(v) => Ok(v), 67 | Err(e) => return Err(anyhow::anyhow!("Cannot retrieve site from state: {}", e)), 68 | } 69 | } 70 | 71 | pub fn find_upload_uuid(&self, uuid: &str) -> AnyResult> { 72 | let uploads = self.get_upload_tasks()?; 73 | for task in uploads.iter() { 74 | if task.uuid == uuid { 75 | return Ok(Some(task.to_owned())); 76 | } 77 | } 78 | 79 | Ok(None) 80 | } 81 | 82 | pub fn push_upload_task(&self, target_task: UploadTask) -> AnyResult<()> { 83 | let mut uploads = self.get_upload_tasks()?; 84 | (*uploads).push(target_task); 85 | 86 | Ok(()) 87 | } 88 | 89 | pub fn remove_upload_task(&self, target_task: UploadTask) -> AnyResult<()> { 90 | let mut uploads = self.get_upload_tasks()?; 91 | (*uploads).retain(|upload| upload.uuid != target_task.uuid); 92 | 93 | Ok(()) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /backend/src/service/auth.rs: -------------------------------------------------------------------------------- 1 | use super::{app_state::AppState, token::AccessToken, token::Token}; 2 | use crate::entity::error::Error; 3 | use crate::util::constants::ACCESS_TOKEN; 4 | use rocket::{ 5 | http::Status, 6 | request::{FromRequest, Outcome}, 7 | Request, 8 | }; 9 | 10 | #[derive(Default, Clone)] 11 | pub struct AuthUser { 12 | pub uid: i64, 13 | pub permission: i8, 14 | } 15 | 16 | #[derive(Default, Clone)] 17 | pub struct AuthAdmin { 18 | pub uid: i64, 19 | } 20 | 21 | #[rocket::async_trait] 22 | impl<'r> FromRequest<'r> for AuthUser { 23 | type Error = Error; 24 | 25 | async fn from_request(req: &'r Request<'_>) -> Outcome { 26 | if let Some(token_str) = req.cookies().get(ACCESS_TOKEN) { 27 | if let Some(state) = req.rocket().state::() { 28 | if let Ok(secret) = state.get_secret() { 29 | if let Ok(token) = AccessToken::decode(token_str.value(), &secret) { 30 | if let Ok(allow_guest) = state.get_allow_guest() { 31 | if token.uid > (0 - allow_guest) as i64 32 | && token.permission > 0 - allow_guest 33 | { 34 | return Outcome::Success(AuthUser { 35 | uid: token.uid, 36 | permission: token.permission, 37 | }); 38 | } else { 39 | return Outcome::Failure(( 40 | Status::Unauthorized, 41 | Error::Unauthorized, 42 | )); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | Outcome::Failure((Status::Unauthorized, Error::Unauthorized)) 51 | } 52 | } 53 | 54 | #[rocket::async_trait] 55 | impl<'r> FromRequest<'r> for AuthAdmin { 56 | type Error = Error; 57 | 58 | async fn from_request(req: &'r Request<'_>) -> Outcome { 59 | if let Some(token_str) = req.cookies().get(ACCESS_TOKEN) { 60 | if let Some(state) = req.rocket().state::() { 61 | if let Ok(secret) = state.get_secret() { 62 | if let Ok(token) = AccessToken::decode(token_str.value(), &secret) { 63 | if token.uid > 0 && token.permission == 9 { 64 | return Outcome::Success(AuthAdmin { uid: token.uid }); 65 | } else { 66 | return Outcome::Failure((Status::Unauthorized, Error::Unauthorized)); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | Outcome::Failure((Status::Unauthorized, Error::Unauthorized)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /backend/src/service/fairings.rs: -------------------------------------------------------------------------------- 1 | use crate::util::constants; 2 | use rocket::fairing::{Fairing, Info, Kind}; 3 | use rocket::http::uri::Path; 4 | use rocket::http::{Method, Status}; 5 | use rocket::{Request, Response}; 6 | 7 | pub struct StaticFileCache; 8 | 9 | #[rocket::async_trait] 10 | impl Fairing for StaticFileCache { 11 | fn info(&self) -> Info { 12 | Info { 13 | name: "Static file cache", 14 | kind: Kind::Response, 15 | } 16 | } 17 | 18 | async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) { 19 | if response.status() != Status::Ok { 20 | return; 21 | } 22 | 23 | let req_path = request.uri().path(); 24 | if request.method() == Method::Get && req_static(&req_path) { 25 | let content = format!("private, max-age={}", constants::CACHE_MAX_AGE); 26 | response.set_raw_header("Cache-Control", content); 27 | response.set_raw_header("Accept-Ranges", "bytes"); 28 | } 29 | } 30 | } 31 | 32 | // Do not cache development related files in debug mode. 33 | #[cfg(debug_assertions)] 34 | fn req_static<'r>(req_path: &Path) -> bool { 35 | req_path.starts_with("/api/file/") && !req_path.starts_with("/api/file/copy-move-status") 36 | } 37 | 38 | #[cfg(not(debug_assertions))] 39 | fn req_static<'r>(req_path: &Path) -> bool { 40 | for ext in constants::CACHE_FILE_EXTS.iter() { 41 | if req_path.ends_with(ext) { 42 | return true; 43 | } 44 | } 45 | 46 | req_path.starts_with("/api/file/") && !req_path.starts_with("/api/file/copy-move-status") 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/service/migrate_dir.rs: -------------------------------------------------------------------------------- 1 | use include_dir::Dir; 2 | use rocket::futures::future::BoxFuture; 3 | use sqlx::{ 4 | error::BoxDynError, 5 | migrate::{Migration, MigrationSource, MigrationType}, 6 | }; 7 | use std::borrow::Cow; 8 | 9 | #[derive(Debug)] 10 | pub struct MigrationDir<'s> { 11 | dir: Dir<'s>, 12 | } 13 | 14 | impl<'s> MigrationDir<'s> { 15 | pub fn new(dir: Dir<'s>) -> Self { 16 | Self { dir } 17 | } 18 | } 19 | 20 | impl<'s> MigrationSource<'s> for MigrationDir<'s> { 21 | fn resolve(self) -> BoxFuture<'s, Result, BoxDynError>> { 22 | Box::pin(async move { 23 | let mut migrations = Vec::new(); 24 | for file in self.dir.files() { 25 | let file_name = file.path().file_name().unwrap().to_string_lossy(); 26 | let parts = file_name.splitn(2, '_').collect::>(); 27 | 28 | if parts.len() != 2 || !parts[1].ends_with(".sql") { 29 | continue; 30 | } 31 | 32 | let version: i64 = parts[0].parse()?; 33 | let migration_type = MigrationType::from_filename(parts[1]); 34 | let description = parts[1] 35 | .trim_end_matches(migration_type.suffix()) 36 | .replace('_', " ") 37 | .to_owned(); 38 | 39 | let sql = String::from_utf8(file.contents().to_vec())?; 40 | 41 | migrations.push(Migration::new( 42 | version, 43 | Cow::Owned(description), 44 | migration_type, 45 | Cow::Owned(sql), 46 | )); 47 | 48 | migrations.sort_by(|a, b| a.version.cmp(&b.version)); 49 | } 50 | 51 | Ok(migrations) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app_state; 2 | pub mod auth; 3 | pub mod fairings; 4 | pub mod migrate_dir; 5 | pub mod range; 6 | pub mod static_route; 7 | pub mod token; 8 | pub mod track; 9 | -------------------------------------------------------------------------------- /backend/src/service/range.rs: -------------------------------------------------------------------------------- 1 | use crate::entity::error::Error; 2 | use anyhow::Result as AnyResult; 3 | use rocket::http::{ContentType, Status}; 4 | use rocket::response::{self, Responder}; 5 | use rocket::Response; 6 | use rocket::{ 7 | request::{FromRequest, Outcome}, 8 | Request, 9 | }; 10 | use std::io::SeekFrom; 11 | use std::path::PathBuf; 12 | use tokio::fs::File; 13 | use tokio::io::{AsyncReadExt, AsyncSeekExt, Take}; 14 | 15 | pub struct Range { 16 | pub range: Option<(u64, u64)>, 17 | } 18 | 19 | pub struct RangedFile { 20 | path: PathBuf, 21 | size: u64, 22 | start: u64, 23 | end: u64, 24 | take: Take, 25 | } 26 | 27 | impl RangedFile { 28 | pub async fn new(range: (u64, u64), path: PathBuf) -> AnyResult { 29 | let (start, mut end) = range; 30 | let mut file = File::open(&path).await?; 31 | let size = file.metadata().await?.len(); 32 | if end == 0 { 33 | end = size - 1; 34 | } 35 | 36 | file.seek(SeekFrom::Start(start)).await?; 37 | let take = file.take(end - start + 1); 38 | 39 | Ok(RangedFile { 40 | path, 41 | size, 42 | start, 43 | end, 44 | take, 45 | }) 46 | } 47 | 48 | fn get_content_type(&self) -> ContentType { 49 | if let Some(ext) = self.path.extension() { 50 | if let Some(ext_str) = ext.to_str() { 51 | if let Some(content_type) = ContentType::from_extension(ext_str) { 52 | return content_type; 53 | } 54 | } 55 | } 56 | 57 | ContentType::Binary 58 | } 59 | 60 | fn get_content_range(&self) -> String { 61 | format!("bytes {}-{}/{}", self.start, self.end, self.size) 62 | } 63 | 64 | fn get_content_len(&self) -> String { 65 | (self.end - self.start + 1).to_string() 66 | } 67 | } 68 | 69 | #[rocket::async_trait] 70 | impl<'r> FromRequest<'r> for Range { 71 | type Error = Error; 72 | 73 | async fn from_request(req: &'r Request<'_>) -> Outcome { 74 | let bad_request = (Status::BadRequest, Error::BadRequest); 75 | let range_header = req.headers().get_one("Range"); 76 | 77 | match range_header { 78 | Some(range_str) => { 79 | let parts: Vec<&str> = range_str.split(|c| c == '=' || c == '-').collect(); 80 | 81 | // start value must be valid. 82 | let start: u64 = match parts.get(1) { 83 | Some(v) => v.parse().unwrap(), 84 | None => return Outcome::Failure(bad_request), 85 | }; 86 | 87 | // end value could be empty, but must not be smaller than start value. 88 | let end: u64 = parts.get(2).unwrap_or(&"0").parse().unwrap_or(0); 89 | if end < start && end != 0 { 90 | return Outcome::Failure(bad_request); 91 | } 92 | 93 | Outcome::Success(Range { 94 | range: Some((start, end)), 95 | }) 96 | } 97 | None => Outcome::Success(Range { range: None }), 98 | } 99 | } 100 | } 101 | 102 | impl<'r, 'o: 'r> Responder<'r, 'o> for RangedFile { 103 | fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'o> { 104 | Response::build() 105 | .status(Status::PartialContent) 106 | .header(self.get_content_type()) 107 | .raw_header("Content-Range", self.get_content_range()) 108 | .raw_header("Content-Length", self.get_content_len()) 109 | .streamed_body(self.take) 110 | .ok() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /backend/src/service/static_route.rs: -------------------------------------------------------------------------------- 1 | use super::token::AccessToken; 2 | use crate::entity::error::Error; 3 | use crate::service::app_state::AppState; 4 | use crate::service::auth::AuthUser; 5 | use crate::util; 6 | use rocket::fs::NamedFile; 7 | use rocket::response::Redirect; 8 | use rocket::{Either, Route, Shutdown, State}; 9 | use std::path::PathBuf; 10 | 11 | pub fn serve() -> Vec { 12 | routes![ 13 | index, 14 | index_html, 15 | shutdown, 16 | login, 17 | setup, 18 | files, 19 | files_all, 20 | settings, 21 | profile, 22 | forgot_password, 23 | reset_password 24 | ] 25 | } 26 | 27 | #[get("/")] 28 | fn index(state: &State) -> Redirect { 29 | handle_index(state.get_first_run()) 30 | } 31 | 32 | #[get("/index.html")] 33 | fn index_html(state: &State) -> Redirect { 34 | handle_index(state.get_first_run()) 35 | } 36 | 37 | fn handle_index(first_run: bool) -> Redirect { 38 | if first_run { 39 | Redirect::temporary(uri!("/setup")) 40 | } else { 41 | Redirect::temporary(uri!("/files")) 42 | } 43 | } 44 | 45 | #[get("/login")] 46 | async fn login() -> Option { 47 | open_index_page().await 48 | } 49 | 50 | #[get("/forgot-password")] 51 | async fn forgot_password() -> Option { 52 | open_index_page().await 53 | } 54 | 55 | #[get("/reset-password/<_uuid>")] 56 | async fn reset_password(_uuid: String) -> Option { 57 | open_index_page().await 58 | } 59 | 60 | #[get("/setup")] 61 | async fn setup(state: &State) -> Result, Error> { 62 | match state.get_first_run() { 63 | true => Ok(open_index_page().await), 64 | false => Err(Error::Forbidden), 65 | } 66 | } 67 | 68 | #[get("/files")] 69 | async fn files(token: AccessToken) -> Either, Redirect> { 70 | handle_files(token).await 71 | } 72 | 73 | #[get("/files/<_dirs..>")] 74 | async fn files_all(token: AccessToken, _dirs: PathBuf) -> Either, Redirect> { 75 | handle_files(token).await 76 | } 77 | 78 | async fn handle_files(token: AccessToken) -> Either, Redirect> { 79 | if token.uid >= 0 && token.permission >= 0 { 80 | Either::Left(open_index_page().await) 81 | } else { 82 | Either::Right(Redirect::temporary(uri!("/login"))) 83 | } 84 | } 85 | 86 | #[get("/settings")] 87 | async fn settings(token: AccessToken) -> Result, Error> { 88 | if token.uid <= 0 || token.permission != 9 { 89 | return Err(Error::Unauthorized); 90 | } 91 | 92 | Ok(open_index_page().await) 93 | } 94 | 95 | #[get("/profile")] 96 | async fn profile(_user: AuthUser) -> Option { 97 | open_index_page().await 98 | } 99 | 100 | #[get("/shutdown")] 101 | fn shutdown(shutdown: Shutdown, token: AccessToken) -> Result<(), Error> { 102 | if token.uid <= 0 || token.permission != 9 { 103 | return Err(Error::Forbidden); 104 | } 105 | 106 | println!("Server shutting down as user required"); 107 | shutdown.notify(); 108 | 109 | Ok(()) 110 | } 111 | 112 | async fn open_index_page() -> Option { 113 | let index = util::get_frontend_path().join("index.html"); 114 | NamedFile::open(index).await.ok() 115 | } 116 | -------------------------------------------------------------------------------- /backend/src/service/token.rs: -------------------------------------------------------------------------------- 1 | use super::app_state::AppState; 2 | use crate::entity::error::Error; 3 | use crate::util; 4 | use crate::util::constants::{ACCESS_TOKEN, ACCESS_TOKEN_MINS, REFRESH_TOKEN, REFRESH_TOKEN_DAYS}; 5 | use anyhow::Result as AnyResult; 6 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 7 | use rocket::{ 8 | http::Status, 9 | request::{FromRequest, Outcome}, 10 | serde::{Deserialize, Serialize}, 11 | Request, 12 | }; 13 | 14 | pub trait Token { 15 | fn encode(&self, secret: &str) -> AnyResult 16 | where 17 | Self: Serialize, 18 | { 19 | let token_string = encode( 20 | &Header::default(), 21 | &self, 22 | &EncodingKey::from_secret(secret.as_bytes()), 23 | )?; 24 | 25 | Ok(token_string) 26 | } 27 | 28 | fn decode(token: &str, secret: &str) -> AnyResult 29 | where 30 | Self: Sized, 31 | for<'de> Self: Deserialize<'de>, 32 | { 33 | let token = decode::( 34 | token, 35 | &DecodingKey::from_secret(secret.as_bytes()), 36 | &Validation::default(), 37 | )?; 38 | 39 | Ok(token.claims) 40 | } 41 | } 42 | 43 | #[derive(Debug, Default, Serialize, Deserialize, Clone)] 44 | #[serde(crate = "rocket::serde")] 45 | pub struct AccessToken { 46 | pub exp: usize, 47 | pub uid: i64, 48 | pub permission: i8, 49 | } 50 | 51 | impl Token for AccessToken {} 52 | 53 | impl AccessToken { 54 | pub fn new(uid: i64, permission: i8) -> Self { 55 | let expire_time = util::get_utc_seconds() + ACCESS_TOKEN_MINS * 60; 56 | 57 | AccessToken { 58 | exp: expire_time as usize, 59 | uid, 60 | permission, 61 | } 62 | } 63 | } 64 | 65 | #[rocket::async_trait] 66 | impl<'r> FromRequest<'r> for AccessToken { 67 | type Error = Error; 68 | 69 | async fn from_request(req: &'r Request<'_>) -> Outcome { 70 | if let Some(token_str) = req.cookies().get(ACCESS_TOKEN) { 71 | if let Some(state) = req.rocket().state::() { 72 | if let Ok(secret) = state.get_secret() { 73 | if let Ok(token) = AccessToken::decode(token_str.value(), &secret) { 74 | return Outcome::Success(token); 75 | } 76 | } 77 | } 78 | } 79 | 80 | Outcome::Success(AccessToken::default()) 81 | } 82 | } 83 | 84 | #[derive(Debug, Default, Serialize, Deserialize, Clone)] 85 | #[serde(crate = "rocket::serde")] 86 | pub struct RefreshToken { 87 | pub exp: usize, 88 | pub uid: i64, 89 | } 90 | 91 | impl Token for RefreshToken {} 92 | 93 | impl RefreshToken { 94 | pub fn new(uid: i64) -> Self { 95 | let expire_time = util::get_utc_seconds() + REFRESH_TOKEN_DAYS * 24 * 60 * 60; 96 | 97 | RefreshToken { 98 | exp: expire_time as usize, 99 | uid, 100 | } 101 | } 102 | } 103 | 104 | #[rocket::async_trait] 105 | impl<'r> FromRequest<'r> for RefreshToken { 106 | type Error = Error; 107 | 108 | async fn from_request(req: &'r Request<'_>) -> Outcome { 109 | if let Some(token_str) = req.cookies().get(REFRESH_TOKEN) { 110 | if let Some(state) = req.rocket().state::() { 111 | if let Ok(secret) = state.get_secret() { 112 | if let Ok(token) = RefreshToken::decode(token_str.value(), &secret) { 113 | return Outcome::Success(token); 114 | } 115 | } 116 | } 117 | } 118 | 119 | Outcome::Failure((Status::BadRequest, Error::BadRequest)) 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use super::*; 126 | 127 | #[test] 128 | fn test_token_should_work() { 129 | let secret = "mySitePassword"; 130 | let claim = AccessToken::new(1, 9); 131 | let token = claim.encode(secret).unwrap(); 132 | let validate = AccessToken::decode(&token, secret).unwrap(); 133 | assert_eq!(validate.permission, 9); 134 | } 135 | 136 | #[test] 137 | #[should_panic] 138 | fn test_token_should_panic() { 139 | let mut claim = AccessToken::new(1, 9); 140 | claim.exp -= 30 * 60; 141 | let token = claim.encode("secret").unwrap(); 142 | let validate = AccessToken::decode(&token, "secret").unwrap(); 143 | assert_eq!(validate.permission, 9); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /backend/src/service/track.rs: -------------------------------------------------------------------------------- 1 | use crate::util::file_system; 2 | use anyhow::Result as AnyResult; 3 | use regex::Regex; 4 | use std::path::PathBuf; 5 | const LIMIT: u64 = 1 * 1024 * 1024; 6 | 7 | pub async fn get_track(vtt_path: PathBuf) -> AnyResult { 8 | if vtt_path.exists() && vtt_path.is_file() { 9 | if vtt_path.metadata()?.len() > LIMIT { 10 | return Err(anyhow::anyhow!("Track file too big")); 11 | } 12 | 13 | return file_system::read_text_file(vtt_path).await; 14 | } 15 | 16 | let srt_path = vtt_path.with_extension("srt"); 17 | if srt_path.exists() && srt_path.is_file() { 18 | if srt_path.metadata()?.len() > LIMIT { 19 | return Err(anyhow::anyhow!("Track file too big")); 20 | } 21 | 22 | let srt_string = file_system::read_text_file(srt_path).await?; 23 | return srt_to_vtt(&srt_string).await; 24 | } 25 | 26 | Err(anyhow::anyhow!("Cannot find track file")) 27 | } 28 | 29 | async fn srt_to_vtt(srt_str: &str) -> AnyResult { 30 | let lines = srt_str.lines(); 31 | 32 | let mut vtt_str = String::from("WEBVTT"); 33 | vtt_str.push('\n'); 34 | vtt_str.push('\n'); 35 | 36 | let regex_digit = Regex::new(r"^\d+$")?; 37 | let regex_time = Regex::new(r"^.*-->.*$")?; 38 | 39 | for line in lines { 40 | if regex_digit.is_match(line) { 41 | continue; 42 | } 43 | 44 | if regex_time.is_match(line) { 45 | let time_line = line.replace(",", "."); 46 | vtt_str.push_str(&time_line); 47 | vtt_str.push('\n'); 48 | } else { 49 | vtt_str.push_str(line); 50 | vtt_str.push('\n'); 51 | } 52 | } 53 | 54 | Ok(vtt_str) 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use super::*; 60 | 61 | #[test] 62 | fn test_srt_to_vtt() { 63 | let srt = r#" 64 | 1 65 | 00:00:06,708 --> 00:00:10,542 66 | 我是永尾 我是永尾完治 67 | 68 | 2 69 | 00:00:11,746 --> 00:00:15,580 70 | 我现在已经到达羽田了 71 | 72 | 3 73 | 00:00:17,752 --> 00:00:20,687 74 | 请问要来接我的人呢 75 | 76 | 4 77 | 00:00:20,755 --> 00:00:24,691 78 | 在入境的出口 女性 79 | 80 | 5 81 | 00:00:24,759 --> 00:00:28,593 82 | 深蓝色的外套 是 83 | "#; 84 | let rt = rocket::tokio::runtime::Runtime::new().unwrap(); 85 | let vtt = rt.block_on(srt_to_vtt(srt)).unwrap(); 86 | 87 | println!("Vtt result:\n {}", &vtt); 88 | assert!(vtt.len() > 0); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /backend/src/util/constants.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, Ipv4Addr}; 2 | 3 | pub const DEFAULT_APP_NAME: &'static str = "Oasis"; 4 | pub const DEFAULT_UPDATE_FREQ: &'static str = "monthly"; 5 | pub const DEFAULT_LANGUAGE: &'static str = "en"; 6 | pub const VERSION: &'static str = "0.2.6"; 7 | #[allow(dead_code)] 8 | pub const FRONTEND_DIR_DEBUG: &'static str = "../frontend/public/"; 9 | #[allow(dead_code)] 10 | pub const FRONTEND_DIR_RELEASE: &'static str = "public"; 11 | pub const ACCESS_TOKEN: &'static str = "oa_access"; 12 | pub const ACCESS_TOKEN_MINS: i64 = 20; 13 | pub const REFRESH_TOKEN: &'static str = "oa_refresh"; 14 | pub const REFRESH_TOKEN_DAYS: i64 = 7; 15 | pub const CACHE_MAX_AGE: i64 = 60 * 60; 16 | #[allow(dead_code)] 17 | pub const APP_VERSION_URL_RELEASE: &'static str = 18 | "https://raw.githubusercontent.com/machengim/oasis/main/version.txt"; 19 | #[allow(dead_code)] 20 | pub const APP_VERSION_URL_DEBUG: &'static str = 21 | "https://raw.githubusercontent.com/machengim/oasis/dev/version.txt"; 22 | #[allow(dead_code)] 23 | pub const CACHE_FILE_EXTS: [&'static str; 3] = ["html", "js", "css"]; 24 | pub const DEFAULT_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); 25 | pub const ZIP_BUFFER_SIZE: usize = 65536; 26 | -------------------------------------------------------------------------------- /backend/src/util/db.rs: -------------------------------------------------------------------------------- 1 | use sqlx::{pool::PoolConnection, sqlite::SqliteRow, Error, FromRow, Sqlite, Transaction}; 2 | 3 | #[derive(Debug)] 4 | pub struct Query<'a> { 5 | pub sql: &'a str, 6 | pub args: Vec, 7 | } 8 | 9 | impl<'a> Query<'a> { 10 | pub fn new(sql: &'a str, args: Vec) -> Self { 11 | Query { sql, args } 12 | } 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! args { 17 | ( $( $x:expr ),* ) => { 18 | { 19 | let mut temp_vec = Vec::new(); 20 | $( 21 | let x_str = $x.to_string(); 22 | temp_vec.push(x_str); 23 | )* 24 | temp_vec 25 | } 26 | }; 27 | } 28 | 29 | pub async fn fetch_single<'r, T>( 30 | query: Query<'r>, 31 | conn: &mut PoolConnection, 32 | ) -> Result, Error> 33 | where 34 | T: Send + Unpin + for<'a> FromRow<'a, SqliteRow>, 35 | { 36 | let stmt = prepare_sql(query.sql, &query.args); 37 | Ok(stmt.fetch_optional(conn).await?) 38 | } 39 | 40 | pub async fn fetch_multiple<'r, T>( 41 | query: Query<'r>, 42 | conn: &mut PoolConnection, 43 | ) -> Result, Error> 44 | where 45 | T: Send + Unpin + for<'a> FromRow<'a, SqliteRow>, 46 | { 47 | let stmt = prepare_sql(query.sql, &query.args); 48 | Ok(stmt.fetch_all(conn).await?) 49 | } 50 | 51 | pub async fn execute<'r>(query: Query<'r>, tx: &mut Transaction<'_, Sqlite>) -> Result { 52 | let mut row_id = 0; 53 | let stmt = prepare_exec_sql(query.sql, &query.args); 54 | if query.sql.to_lowercase().starts_with("insert") { 55 | row_id = stmt.execute(&mut *tx).await?.last_insert_rowid(); 56 | } else { 57 | stmt.execute(&mut *tx).await?; 58 | } 59 | 60 | Ok(row_id) 61 | } 62 | 63 | fn prepare_sql<'a, T>( 64 | sql: &'a str, 65 | args: &'a Vec, 66 | ) -> sqlx::query::QueryAs<'a, sqlx::Sqlite, T, sqlx::sqlite::SqliteArguments<'a>> 67 | where 68 | T: Send + Unpin + for<'b> FromRow<'b, SqliteRow>, 69 | { 70 | let mut stmt = sqlx::query_as(sql); 71 | for arg in args.iter() { 72 | stmt = stmt.bind(arg); 73 | } 74 | 75 | stmt 76 | } 77 | 78 | fn prepare_exec_sql<'a>( 79 | sql: &'a str, 80 | args: &'a Vec, 81 | ) -> sqlx::query::Query<'a, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'a>> { 82 | let mut stmt = sqlx::query(sql); 83 | for arg in args.iter() { 84 | stmt = stmt.bind(arg); 85 | } 86 | 87 | stmt 88 | } 89 | -------------------------------------------------------------------------------- /backend/src/util/file_system.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result as AnyResult; 2 | use chardetng::EncodingDetector; 3 | use encoding_rs::Encoding; 4 | use rocket::tokio::fs; 5 | use std::path::PathBuf; 6 | use sysinfo::{DiskExt, System, SystemExt}; 7 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 8 | 9 | pub fn get_system_volumes() -> AnyResult> { 10 | let mut sys = System::new_all(); 11 | sys.refresh_all(); 12 | 13 | let mut volumes = vec![]; 14 | for disk in sys.disks() { 15 | volumes.push(disk.mount_point().to_string_lossy().to_string()); 16 | } 17 | 18 | Ok(volumes) 19 | } 20 | 21 | pub async fn get_sub_dirs(dir: &PathBuf) -> AnyResult> { 22 | if !dir.is_dir() { 23 | return Err(anyhow::anyhow!("Not a directory!")); 24 | } 25 | 26 | let mut dir_iterator = fs::read_dir(dir).await?; 27 | let mut sub_dirs: Vec = Vec::new(); 28 | while let Some(entry) = dir_iterator.next_entry().await? { 29 | let path = entry.path(); 30 | if path.is_dir() { 31 | sub_dirs.push(path); 32 | } 33 | } 34 | 35 | Ok(sub_dirs) 36 | } 37 | 38 | pub fn get_available_space(storage: &str) -> u64 { 39 | let mut sys = System::new_all(); 40 | sys.refresh_all(); 41 | 42 | for disk in sys.disks() { 43 | if storage.starts_with(&disk.mount_point().to_string_lossy().to_string()) { 44 | return disk.available_space(); 45 | } 46 | } 47 | 48 | return 0; 49 | } 50 | 51 | // All text file needs to check the encoding method. 52 | pub async fn read_text_file(path: PathBuf) -> AnyResult { 53 | let mut buffer = vec![]; 54 | let mut file = fs::File::open(path).await?; 55 | file.read_to_end(&mut buffer).await?; 56 | 57 | let encoding = detect_encoding(&buffer)?; 58 | let (cow, _encoding, malformed) = encoding.decode(&buffer); 59 | if malformed { 60 | return Err(anyhow::anyhow!("File encoding malformed")); 61 | } 62 | 63 | Ok(cow.to_owned().to_string()) 64 | } 65 | 66 | fn detect_encoding(buffer: &[u8]) -> AnyResult<&'static Encoding> { 67 | let mut detector = EncodingDetector::new(); 68 | detector.feed(buffer, true); 69 | let (encoding, liable) = detector.guess_assess(None, true); 70 | if !liable { 71 | return Err(anyhow::anyhow!("Cannot detect encoding method")); 72 | } 73 | 74 | Ok(encoding) 75 | } 76 | 77 | pub async fn write_text_file(path: &PathBuf, content: &str) -> AnyResult<()> { 78 | let mut file = fs::File::create(path).await?; 79 | file.write_all(content.as_bytes()).await?; 80 | 81 | Ok(()) 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | 88 | #[cfg(target_os = "linux")] 89 | #[test] 90 | fn test_get_sub_directories() { 91 | use tokio::runtime::Runtime; 92 | 93 | let path = PathBuf::from("/home"); 94 | let rt = Runtime::new().unwrap(); 95 | let sub_directories = rt.block_on(get_sub_dirs(&path)).unwrap(); 96 | 97 | println!("sub_directories: {:?}", &sub_directories); 98 | assert!(sub_directories.len() > 0); 99 | } 100 | 101 | #[test] 102 | fn test_get_file_encoding() { 103 | let pwd = std::env::current_dir().unwrap(); 104 | let path = pwd.join("assets/tests/01.srt"); 105 | let rt = rocket::tokio::runtime::Runtime::new().unwrap(); 106 | let decoded_str = rt.block_on(read_text_file(path)).unwrap(); 107 | println!("Decoded string: {}", &decoded_str); 108 | assert!(decoded_str.len() > 0); 109 | } 110 | 111 | #[test] 112 | #[cfg(target_os = "linux")] 113 | fn test_disk_space() { 114 | let storage = "/home"; 115 | let space = get_available_space(storage); 116 | println!("Space in {} is {}", storage, space); 117 | assert!(space > 0); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /backend/src/util/init.rs: -------------------------------------------------------------------------------- 1 | use super::constants; 2 | use crate::util; 3 | use crate::{entity::site::Site, service::migrate_dir::MigrationDir}; 4 | use anyhow::Result as AnyResult; 5 | use include_dir::{include_dir, Dir}; 6 | use rocket::tokio::fs; 7 | use sqlx::{ 8 | migrate::Migrator, pool::PoolConnection, sqlite::SqliteConnectOptions, ConnectOptions, 9 | Connection, Sqlite, SqliteConnection, SqlitePool, 10 | }; 11 | use std::path::PathBuf; 12 | 13 | pub async fn init_app() -> AnyResult<()> { 14 | // Remove temp dir and create it again at every startup. 15 | let temp_dir = util::get_temp_path(); 16 | if !temp_dir.exists() { 17 | fs::create_dir_all(&temp_dir).await?; 18 | } else if temp_dir.is_file() { 19 | return Err(anyhow::anyhow!("temp dir location occupied as a file")); 20 | } else { 21 | fs::remove_dir_all(&temp_dir).await?; 22 | fs::create_dir_all(&temp_dir).await?; 23 | } 24 | 25 | // Create db and run migration files if db not existed. 26 | let db_file = get_db_file_location(); 27 | if !db_file.exists() { 28 | let db_dir = db_file.parent().unwrap(); 29 | if !db_dir.exists() { 30 | fs::create_dir_all(db_dir).await?; 31 | } 32 | 33 | let mut conn = get_db_conn(db_file).await?; 34 | run_migration(&mut conn).await?; 35 | conn.close(); 36 | } 37 | 38 | Ok(()) 39 | } 40 | 41 | pub async fn check_update(conn: &mut PoolConnection) -> AnyResult<()> { 42 | run_migration(conn).await?; 43 | 44 | let mut site = match Site::read(conn).await? { 45 | Some(s) => s, 46 | None => return Ok(()), 47 | }; 48 | 49 | let version_db = site.version; 50 | let version_app = constants::VERSION; 51 | 52 | if compare_version(version_app, &version_db) > 0 { 53 | site.version = version_app.to_owned(); 54 | let mut tx = conn.begin().await?; 55 | site.update(&mut tx).await?; 56 | tx.commit().await?; 57 | } 58 | 59 | println!("Oasis version {}", constants::VERSION); 60 | Ok(()) 61 | } 62 | 63 | async fn get_db_conn(db_file: PathBuf) -> AnyResult { 64 | Ok(SqliteConnectOptions::new() 65 | .filename(db_file) 66 | .create_if_missing(true) 67 | .connect() 68 | .await?) 69 | } 70 | 71 | async fn run_migration(conn: &mut SqliteConnection) -> AnyResult<()> { 72 | const ASSETS: Dir = include_dir!("./assets"); 73 | let migration_dir = ASSETS 74 | .get_dir("migrations") 75 | .ok_or(anyhow::anyhow!("Migration dir not found"))?; 76 | let migrator = Migrator::new(MigrationDir::new(migration_dir)).await?; 77 | migrator.run(conn).await?; 78 | 79 | Ok(()) 80 | } 81 | 82 | pub async fn get_db_pool() -> Result { 83 | let db_file = get_db_file_location(); 84 | let option = SqliteConnectOptions::new().filename(db_file); 85 | let pool = SqlitePool::connect_with(option).await?; 86 | 87 | Ok(pool) 88 | } 89 | 90 | fn get_db_file_location() -> PathBuf { 91 | let pwd = super::get_pwd(); 92 | // Change in v0.2.4, move db directory to data directory. 93 | pwd.join("data").join("db").join("main.db") 94 | } 95 | 96 | fn compare_version(va: &str, vb: &str) -> i8 { 97 | use std::cmp::min; 98 | 99 | let parts_a: Vec = va.split(".").map(|e| e.parse().unwrap()).collect(); 100 | let parts_b: Vec = vb.split(".").map(|e| e.parse().unwrap()).collect(); 101 | let len_a = parts_a.len(); 102 | let len_b = parts_b.len(); 103 | 104 | for i in 0..min(len_a, len_b) { 105 | if parts_a[i] > parts_b[i] { 106 | return 1; 107 | } else if parts_a[i] < parts_b[i] { 108 | return -1; 109 | } 110 | } 111 | 112 | // Assume no version length more than 128 113 | len_a as i8 - len_b as i8 114 | } 115 | 116 | #[cfg(test)] 117 | mod test { 118 | use super::*; 119 | 120 | #[test] 121 | fn test_compare_version() { 122 | let va = "0.1.1"; 123 | let vb = "0.1"; 124 | assert!(compare_version(va, vb) > 0); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /backend/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod db; 3 | pub mod file_system; 4 | pub mod init; 5 | pub mod local_ip; 6 | pub mod rocket_env; 7 | use anyhow::Result as AnyResult; 8 | use rand::{distributions::Alphanumeric, Rng}; 9 | use sha2::{Digest, Sha256}; 10 | use std::path::PathBuf; 11 | 12 | pub fn generate_secret_key(length: usize) -> String { 13 | rand::thread_rng() 14 | .sample_iter(&Alphanumeric) 15 | .take(length) 16 | .map(char::from) 17 | .collect() 18 | } 19 | 20 | pub fn parse_encoded_url(url: &str) -> AnyResult { 21 | let url_decode = urlencoding::decode(url)?; 22 | 23 | Ok(PathBuf::from(url_decode.into_owned())) 24 | } 25 | 26 | pub fn get_frontend_path() -> PathBuf { 27 | let path = get_frontend_dir(); 28 | 29 | if !path.exists() || !path.is_dir() { 30 | panic!("Invalid frontend directory"); 31 | } 32 | 33 | path 34 | } 35 | 36 | pub fn get_temp_path() -> PathBuf { 37 | get_pwd().join("temp") 38 | } 39 | 40 | pub fn get_data_temp_path() -> PathBuf { 41 | get_pwd().join("data").join("temp") 42 | } 43 | 44 | #[cfg(debug_assertions)] 45 | pub fn get_frontend_dir() -> PathBuf { 46 | let front_dir = constants::FRONTEND_DIR_DEBUG; 47 | PathBuf::from(front_dir) 48 | } 49 | 50 | #[cfg(not(debug_assertions))] 51 | pub fn get_frontend_dir() -> PathBuf { 52 | let front_dir = constants::FRONTEND_DIR_RELEASE; 53 | let pwd = get_pwd(); 54 | pwd.join(front_dir) 55 | } 56 | 57 | #[cfg(debug_assertions)] 58 | pub fn get_verion_url() -> String { 59 | String::from(constants::APP_VERSION_URL_DEBUG) 60 | } 61 | 62 | #[cfg(not(debug_assertions))] 63 | pub fn get_verion_url() -> String { 64 | String::from(constants::APP_VERSION_URL_RELEASE) 65 | } 66 | 67 | pub fn get_pwd() -> PathBuf { 68 | let exe_file = std::env::current_exe().expect("Cannot get app directory"); 69 | exe_file 70 | .parent() 71 | .expect("Cannot get parent dir of exe file") 72 | .to_path_buf() 73 | } 74 | 75 | pub fn get_version_constant() -> String { 76 | constants::VERSION.to_string() 77 | } 78 | 79 | pub fn sha256(input: &str, secret: &str) -> String { 80 | let mut hasher = Sha256::new(); 81 | let input_with_secret = format!("{}&key={}", input, secret); 82 | hasher.update(input_with_secret); 83 | let result = hasher.finalize(); 84 | format!("{:X}", &result) 85 | } 86 | 87 | pub fn get_utc_seconds() -> i64 { 88 | chrono::Utc::now().timestamp() 89 | } 90 | 91 | #[cfg(test)] 92 | mod test { 93 | use super::*; 94 | 95 | #[test] 96 | fn test_sha256() { 97 | let input = "Hello world"; 98 | let sha_result = sha256(input, "12ab34cd"); 99 | println!("sha result is {}", &sha_result); 100 | assert!(sha_result.len() > 0); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /backend/src/util/rocket_env.rs: -------------------------------------------------------------------------------- 1 | use super::local_ip::ServerConfig; 2 | use std::collections::HashMap; 3 | 4 | // use this instead of `dotenv` because of the conflict 5 | // when double clicking the app to launch in MacOS, 6 | // dotenv doesn't work properly. 7 | pub struct RocketEnv<'a> { 8 | pub vars: HashMap<&'a str, &'a str>, 9 | } 10 | 11 | impl<'a> RocketEnv<'a> { 12 | #[cfg(not(debug_assertions))] 13 | pub fn setup(config: &ServerConfig) { 14 | let ip_str = config.ip.to_string(); 15 | let port_str = config.port.to_string(); 16 | let tls_str; 17 | 18 | let mut vars = HashMap::new(); 19 | vars.insert("ROCKET_ADDRESS", &ip_str[..]); 20 | vars.insert("ROCKET_PORT", &port_str[..]); 21 | vars.insert("ROCKET_WORKERS", "8"); 22 | vars.insert("ROCKET_KEEP_ALIVE", "4"); 23 | vars.insert("ROCKET_LOG_LEVEL", "off"); 24 | vars.insert("ROCKET_LIMITS", "{file=\"8 MiB\"}"); 25 | if config.certs.is_some() && config.key.is_some() { 26 | tls_str = config.get_tls_str(); 27 | vars.insert("ROCKET_TLS", &tls_str); 28 | } 29 | 30 | let env = RocketEnv { vars }; 31 | env.set_var(); 32 | } 33 | 34 | #[cfg(debug_assertions)] 35 | pub fn setup(config: &ServerConfig) { 36 | let ip_str = config.ip.to_string(); 37 | let port_str = config.port.to_string(); 38 | let tls_str; 39 | 40 | let mut vars = HashMap::new(); 41 | vars.insert("ROCKET_ADDRESS", &ip_str[..]); 42 | vars.insert("ROCKET_PORT", &port_str[..]); 43 | vars.insert("ROCKET_WORKERS", "2"); 44 | vars.insert("ROCKET_KEEP_ALIVE", "1"); 45 | vars.insert("ROCKET_LOG_LEVEL", "debug"); 46 | vars.insert("ROCKET_LIMITS", "{file=\"8 MiB\"}"); 47 | if config.certs.is_some() && config.key.is_some() { 48 | tls_str = config.get_tls_str(); 49 | vars.insert("ROCKET_TLS", &tls_str); 50 | } 51 | 52 | let env = RocketEnv { vars }; 53 | env.set_var(); 54 | } 55 | 56 | fn set_var(&self) { 57 | for (key, value) in &self.vars { 58 | std::env::set_var(key, value); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const process = require("process"); 3 | const child_process = require("child_process"); 4 | const path = require("path"); 5 | 6 | const copyRecursiveSync = (src, dest) => { 7 | const exists = fs.existsSync(src); 8 | const stats = exists && fs.statSync(src); 9 | const isDirectory = exists && stats.isDirectory(); 10 | if (isDirectory) { 11 | fs.mkdirSync(dest); 12 | fs.readdirSync(src).forEach(function (childItemName) { 13 | copyRecursiveSync(path.join(src, childItemName), 14 | path.join(dest, childItemName)); 15 | }); 16 | } else { 17 | fs.copyFileSync(src, dest); 18 | } 19 | }; 20 | 21 | const runCommand = (cmd) => { 22 | console.log("\n", cmd); 23 | child_process.execSync(cmd, { stdio: 'inherit' }); 24 | } 25 | 26 | const createReleaseDir = () => { 27 | if (fs.existsSync("release")) { 28 | fs.rmSync("release", { recursive: true, force: true }); 29 | } 30 | 31 | fs.mkdirSync("release/oasis", { recursive: true }, (e) => { 32 | if (e) { 33 | throw e; 34 | } 35 | }); 36 | } 37 | 38 | const filename = process.platform.startsWith("win") ? "oasis.exe" : "oasis"; 39 | 40 | createReleaseDir(); 41 | 42 | process.chdir("frontend"); 43 | runCommand("npm i"); 44 | runCommand("npm run build"); 45 | copyRecursiveSync("public", "../release/oasis/public"); 46 | 47 | process.chdir("../backend"); 48 | runCommand("cargo build --release"); 49 | copyRecursiveSync("target/release/" + filename, "../release/oasis/" + filename); 50 | copyRecursiveSync("assets/oasis.conf.sample", "../release/oasis/oasis.conf.sample"); 51 | 52 | process.chdir("../release/oasis"); 53 | fs.chmodSync(filename, 0o755); 54 | 55 | console.log("\nBuild complete. Please check the 'release' directory."); -------------------------------------------------------------------------------- /doc/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machengim/oasis/3555fe8581ded6e0206f2718e0517091135253ad/doc/demo.png -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | COPY oasis /opt/oasis 3 | CMD /opt/oasis/oasis -------------------------------------------------------------------------------- /docker/oasis-v0.2-alpine.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machengim/oasis/3555fe8581ded6e0206f2718e0517091135253ad/docker/oasis-v0.2-alpine.zip -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | package-lock.json 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "exec": "npm run build", 4 | "ext": "js, json, ts, css, svelte, html" 5 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear", 9 | "check": "svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^17.0.0", 13 | "@rollup/plugin-json": "^4.1.0", 14 | "@rollup/plugin-node-resolve": "^11.0.0", 15 | "@rollup/plugin-typescript": "^8.0.0", 16 | "@tsconfig/svelte": "^2.0.0", 17 | "@types/pdfobject": "^2.2.2", 18 | "autoprefixer": "^10.3.0", 19 | "postcss": "^8.3.5", 20 | "postcss-load-config": "^3.1.0", 21 | "rollup": "^2.3.4", 22 | "rollup-plugin-css-only": "^3.1.0", 23 | "rollup-plugin-livereload": "^2.0.0", 24 | "rollup-plugin-svelte": "^7.0.0", 25 | "rollup-plugin-terser": "^7.0.0", 26 | "svelte": "^3.0.0", 27 | "svelte-check": "^2.0.0", 28 | "svelte-preprocess": "^4.7.4", 29 | "tailwindcss": "^2.2.4", 30 | "tslib": "^2.0.0", 31 | "typescript": "^4.0.0" 32 | }, 33 | "dependencies": { 34 | "copy-to-clipboard": "^3.3.1", 35 | "pdfobject": "^2.2.6", 36 | "plyr": "^3.6.8", 37 | "screenfull": "^5.1.0", 38 | "sirv-cli": "^1.0.0", 39 | "svelte-i18n": "^3.3.10", 40 | "svelte-navigator": "^3.1.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machengim/oasis/3555fe8581ded6e0206f2718e0517091135253ad/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/global.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 16px; 3 | color: rgb(107, 114, 128); 4 | } 5 | 6 | body { 7 | width: 100vw; 8 | overflow-x: hidden; 9 | } 10 | 11 | * { 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | input:disabled { 17 | cursor: not-allowed; 18 | } 19 | 20 | .border-circle { 21 | border-radius: 50%; 22 | } 23 | 24 | .container-height { 25 | height: calc(100vh - 4rem); 26 | } 27 | 28 | .center { 29 | position: absolute; 30 | left: 50%; 31 | top: 45%; 32 | transform: translate(-50%, -50%); 33 | } 34 | 35 | :root { 36 | --plyr-font-size-small: 1rem; 37 | --plyr-font-size-base: 1.3rem; 38 | --plyr-font-size-large: 1.6rem; 39 | --plyr-font-size-xlarge: 2rem; 40 | } 41 | 42 | @media only screen and (min-width: 320px) { 43 | .viewer-height { 44 | height: 60vh; 45 | } 46 | } 47 | 48 | @media only screen and (min-width: 1024px) { 49 | .viewer-height { 50 | height: 80vh; 51 | } 52 | } -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Oasis 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/public/js/md5_worker.js: -------------------------------------------------------------------------------- 1 | importScripts("/vendor/md5/md5.js"); 2 | 3 | self.onmessage = async (e) => { 4 | const file = e.data; 5 | const buffer = await file.arrayBuffer(); 6 | let dataArray = new Uint8Array(buffer); 7 | const hash = md5(dataArray); 8 | self.postMessage(hash); 9 | } -------------------------------------------------------------------------------- /frontend/public/js/upload_worker.js: -------------------------------------------------------------------------------- 1 | self.onmessage = async (e) => { 2 | const { task, start } = e.data; 3 | const file = task.file; 4 | const sliceLength = 2 * 1024 * 1024; 5 | const end = Math.min(start + sliceLength, file.size); 6 | const index = Math.round(start / sliceLength) + 1; 7 | const buffer = await file.slice(start, end).arrayBuffer(); 8 | const endpoint = `/api/upload/${task.uuid}/${index}`; 9 | 10 | const xhr = new XMLHttpRequest(); 11 | 12 | xhr.open('POST', endpoint); 13 | 14 | xhr.onload = (_) => { 15 | self.postMessage(end); 16 | } 17 | 18 | xhr.onerror = (e) => { 19 | throw e; 20 | } 21 | 22 | xhr.send(buffer); 23 | } -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-help.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 16 | 18 | 21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-insert.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-key.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-newparagraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-noicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 14 | 21 | 28 | 35 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/annotation-paragraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/findbarButton-next.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/findbarButton-previous.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/grab.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machengim/oasis/3555fe8581ded6e0206f2718e0517091135253ad/frontend/public/vendor/pdfjs/web/images/grab.cur -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/grabbing.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machengim/oasis/3555fe8581ded6e0206f2718e0517091135253ad/frontend/public/vendor/pdfjs/web/images/grabbing.cur -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/loading-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/loading-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machengim/oasis/3555fe8581ded6e0206f2718e0517091135253ad/frontend/public/vendor/pdfjs/web/images/loading-icon.gif -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-documentProperties.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | 8 | 9 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-firstPage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-handTool.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-lastPage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-rotateCcw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-rotateCw.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-scrollHorizontal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-scrollVertical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-scrollWrapped.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-selectTool.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-spreadEven.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-spreadNone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/secondaryToolbarButton-spreadOdd.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machengim/oasis/3555fe8581ded6e0206f2718e0517091135253ad/frontend/public/vendor/pdfjs/web/images/shadow.png -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-bookmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-currentOutlineItem.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-download.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-menuArrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-openFile.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-pageDown.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-pageUp.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-presentationMode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-print.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-search.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-secondaryToolbarToggle.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-sidebarToggle.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-viewAttachments.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-viewLayers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-viewOutline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-viewThumbnail.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-zoomIn.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/toolbarButton-zoomOut.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/treeitem-collapsed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vendor/pdfjs/web/images/treeitem-expanded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import sveltePreprocess from 'svelte-preprocess'; 7 | import typescript from '@rollup/plugin-typescript'; 8 | import css from 'rollup-plugin-css-only'; 9 | import json from '@rollup/plugin-json'; 10 | 11 | const production = !process.env.ROLLUP_WATCH; 12 | 13 | function serve() { 14 | let server; 15 | 16 | function toExit() { 17 | if (server) server.kill(0); 18 | } 19 | 20 | return { 21 | writeBundle() { 22 | if (server) return; 23 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 24 | stdio: ['ignore', 'inherit', 'inherit'], 25 | shell: true 26 | }); 27 | 28 | process.on('SIGTERM', toExit); 29 | process.on('exit', toExit); 30 | } 31 | }; 32 | } 33 | 34 | export default { 35 | input: 'src/main.ts', 36 | output: { 37 | sourcemap: true, 38 | format: 'iife', 39 | name: 'app', 40 | file: 'public/build/bundle.js' 41 | }, 42 | plugins: [ 43 | svelte({ 44 | preprocess: sveltePreprocess({ sourceMap: !production, postcss: true }), 45 | compilerOptions: { 46 | // enable run-time checks when not in production 47 | dev: !production 48 | } 49 | }), 50 | // we'll extract any component CSS out into 51 | // a separate file - better for performance 52 | css({ output: 'bundle.css' }), 53 | 54 | // If you have external dependencies installed from 55 | // npm, you'll most likely need these plugins. In 56 | // some cases you'll need additional configuration - 57 | // consult the documentation for details: 58 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 59 | resolve({ 60 | browser: true, 61 | dedupe: ['svelte'] 62 | }), 63 | commonjs(), 64 | json(), 65 | typescript({ 66 | sourceMap: !production, 67 | inlineSources: !production 68 | }), 69 | 70 | // In dev mode, call `npm run start` once 71 | // the bundle has been generated 72 | !production && serve(), 73 | 74 | // Watch the `public` directory and refresh the 75 | // browser on changes when not in production 76 | !production && livereload('public'), 77 | 78 | // If we're building for production (npm run build 79 | // instead of npm run dev), minify 80 | production && terser() 81 | ], 82 | watch: { 83 | clearScreen: false 84 | }, 85 | inlineDynamicImports: true 86 | }; 87 | -------------------------------------------------------------------------------- /frontend/src/assets/constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Oasis", 3 | "repo": { 4 | "name": "Github repo", 5 | "url": "https://github.com/machengim/oasis" 6 | }, 7 | "links": [ 8 | { 9 | "name": "Svelte", 10 | "url": "https://svelte.dev" 11 | }, 12 | { 13 | "name": "Rocket", 14 | "url": "https://rocket.rs" 15 | }, 16 | { 17 | "name": "Tailwind", 18 | "url": "https://tailwindcss.com/" 19 | }, 20 | { 21 | "name": "PDFjs", 22 | "url": "https://mozilla.github.io/pdf.js/" 23 | }, 24 | { 25 | "name": "Plyr", 26 | "url": "https://plyr.io/" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /frontend/src/components/BreadCrum.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 | {$t("section." + $sectionStore)} 34 | {#each dirs as dir, i} 35 | / 36 | {dir} 41 | {/each} 42 | {#if filename} 43 | / 44 | {filename} 48 | {/if} 49 |
50 | -------------------------------------------------------------------------------- /frontend/src/components/Button.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /frontend/src/components/FileIcon.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /frontend/src/components/Modal.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
{}} 18 | > 19 | 37 |
38 | 39 | 79 | -------------------------------------------------------------------------------- /frontend/src/components/Spinner.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
8 | 15 | 16 | 21 | 22 | {text} 23 |
24 | -------------------------------------------------------------------------------- /frontend/src/components/Switch.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | 17 |
18 | 19 | 80 | -------------------------------------------------------------------------------- /frontend/src/components/Tailwind.svelte: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /frontend/src/i18n.js: -------------------------------------------------------------------------------- 1 | import { register } from "svelte-i18n"; 2 | 3 | register('en', () => import('./assets/i18n/en.json')); 4 | register('cn', () => import('./assets/i18n/cn.json')); 5 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body 5 | }); 6 | 7 | export default app; -------------------------------------------------------------------------------- /frontend/src/modals/AboutModal.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | 48 |
49 |
50 | {$t("modal.about.version")}: 51 | {version} 52 |
53 | {#if repo} 54 |
55 | {$t("modal.about.repo")}: 56 | 61 | {repo.name} 62 | 63 |
64 | {/if} 65 |
{$t("modal.about.thanks")} ❤
66 |
67 | {#each links as link} 68 | {link.name} 74 | |{" "} 75 | {/each} 76 | ... 77 |
78 |
79 | 80 |
81 | {#if user.permission === 9} 82 |
94 |
95 | -------------------------------------------------------------------------------- /frontend/src/modals/CopyMoveFileModal.svelte: -------------------------------------------------------------------------------- 1 | 66 | 67 | 73 | {#if step === 1} 74 |
75 |
76 | {$t("modal.copy_move.text1")}{mode === "copy" 77 | ? $t("modal.copy_move.copying") 78 | : $t("modal.copy_move.moving")}{source.file_type === EFileType.Dir 79 | ? $t("common.folder") 80 | : $t("common.file")} 81 | {source.filename}{$t("modal.copy_move.text2")} 82 | {target || "/"} 83 |
84 |
85 | {$t("modal.copy_move.text3")} 86 |
87 | 91 |
92 | 93 |
94 |
107 | {:else} 108 |
109 |
110 | {$t(`modal.copy_move.${mode}`)}{source.filename}{$t( 111 | `modal.copy_move.to` 112 | )}{target} 113 |
114 |
115 | {$t("modal.copy_move.progress")}{(progress * 100).toFixed(2)}% 116 |
117 | {#if done} 118 |
{$t("modal.copy_move.done_text")}
119 | {:else if error} 120 |
{$t("modal.copy_move.failed_text")}
121 | {/if} 122 |
123 |
124 |
131 | {/if} 132 |
133 | -------------------------------------------------------------------------------- /frontend/src/modals/DeleteFileModal.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 |
43 | {$t("modal.delete_file.text_before")}{fileType} 44 | {contextFile.filename}{$t("modal.delete_file.text_after")} 45 |
46 | 47 |
48 |
57 |
58 | -------------------------------------------------------------------------------- /frontend/src/modals/FileLinkModal.svelte: -------------------------------------------------------------------------------- 1 | 101 | 102 | 103 | {#if isLoading} 104 | 105 | {:else} 106 |
107 |
{$t("modal.file_share.file")}: {filename}
108 |
109 | {$t("modal.file_share.valid_period")}: 6 {$t("modal.file_share.hours")} 110 |
111 | 118 |
119 |
120 |