├── .babelrc.json ├── .browserslistrc ├── .cloudcmd.menu.js ├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ └── issue_template.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── docker.yml │ └── nodejs.yml ├── .gitignore ├── .madrun.mjs ├── .npmignore ├── .nvmrc ├── .nycrc.json ├── .putout.json ├── .typos.toml ├── .webpack ├── css.js ├── html.js └── js.js ├── .yaspellerrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ChangeLog ├── HELP.md ├── LICENSE ├── README.md ├── app.json ├── bin ├── cloudcmd.mjs └── release.mjs ├── client ├── client.js ├── cloudcmd.js ├── css.js ├── dom │ ├── buffer.js │ ├── current-file.js │ ├── current-file.spec.js │ ├── dialog.js │ ├── directory.js │ ├── dom-tree.js │ ├── dom-tree.spec.js │ ├── events │ │ ├── event-store.js │ │ ├── event-store.spec.js │ │ └── index.js │ ├── files.js │ ├── images.js │ ├── index.js │ ├── index.spec.js │ ├── io │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── send-request.js │ │ └── send-request.spec.js │ ├── load-remote.js │ ├── load.js │ ├── operations │ │ ├── rename-current.js │ │ └── rename-current.spec.js │ ├── rest.js │ ├── select-by-pattern.js │ ├── storage.js │ ├── storage.spec.js │ └── upload-files.js ├── get-json-from-file-table.js ├── key │ ├── binder.js │ ├── index.js │ ├── index.spec.js │ ├── key.js │ ├── set-current-by-char.js │ └── vim │ │ ├── find.js │ │ ├── find.spec.js │ │ ├── globals.fixture.js │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── set-current.js │ │ ├── vim.js │ │ └── vim.spec.js ├── listeners │ ├── get-index.js │ ├── get-range.js │ └── index.js ├── load-module.js ├── modules │ ├── cloud.js │ ├── command-line.js │ ├── config │ │ ├── index.js │ │ └── input.js │ ├── contact.js │ ├── edit-file-vim.js │ ├── edit-file.js │ ├── edit-names-vim.js │ ├── edit-names.js │ ├── edit.js │ ├── help.js │ ├── konsole.js │ ├── markdown.js │ ├── menu.js │ ├── operation │ │ ├── format.js │ │ ├── get-next-current-name.js │ │ ├── index.js │ │ ├── remove-extension.js │ │ ├── remove-extension.spec.js │ │ └── set-listeners.js │ ├── polyfill.js │ ├── polyfill.spec.js │ ├── terminal-run.js │ ├── terminal.js │ ├── upload.js │ ├── user-menu │ │ ├── get-user-menu.js │ │ ├── get-user-menu.spec.js │ │ ├── index.js │ │ ├── navigate.js │ │ ├── navigate.spec.js │ │ ├── parse-error.js │ │ ├── parse-error.spec.js │ │ ├── parse-user-menu.js │ │ ├── parse-user-menu.spec.js │ │ ├── run.js │ │ └── run.spec.js │ └── view │ │ ├── get-type.js │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── types.js │ │ └── types.spec.js ├── sort.js └── sw │ ├── register.js │ ├── register.spec.js │ └── sw.js ├── common ├── base64.js ├── base64.spec.js ├── callbackify.js ├── callbackify.spec.js ├── cloudfunc.js ├── cloudfunc.spec.js ├── datetime.js ├── datetime.spec.js ├── entity.js ├── entity.spec.js ├── try-to-promise-all.js ├── try-to-promise-all.spec.js ├── util.js └── util.spec.js ├── css ├── columns │ ├── name-size-date.css │ └── name-size.css ├── config.css ├── help.css ├── icons.css ├── main.css ├── nojs.css ├── query.css ├── reset.css ├── style.css ├── supports.css ├── terminal.css ├── themes │ ├── dark.css │ └── light.css ├── urls.css ├── user-menu.css └── view.css ├── cssnano.config.js ├── docker-compose.yml ├── docker ├── Dockerfile └── Dockerfile.alpine ├── favicon.ico ├── font ├── DroidSansMono.eot ├── DroidSansMono.woff ├── DroidSansMono.woff2 ├── fontello.eot ├── fontello.json ├── fontello.svg ├── fontello.ttf ├── fontello.woff └── fontello.woff2 ├── html └── index.html ├── img ├── archive-link.png ├── archive.png ├── directory-link.png ├── directory.png ├── favicon │ ├── favicon-256.png │ ├── favicon-big.png │ ├── favicon-notify.png │ ├── favicon.ai │ ├── favicon.cdr │ ├── favicon.eps │ └── favicon.png ├── file-link.png ├── file.png ├── logo │ ├── cloudcmd-hq.png │ ├── cloudcmd.cdr │ └── cloudcmd.png ├── screen │ ├── config.png │ ├── console.png │ ├── edit.png │ ├── menu.png │ ├── one-file-panel.png │ ├── terminal.png │ └── view.png ├── spinner.gif └── spinner.svg ├── json ├── config.json ├── help.json └── modules.json ├── man └── cloudcmd.1 ├── manifest.yml ├── package.json ├── public └── manifest.json ├── server ├── auth.js ├── cloudcmd.mjs ├── cloudcmd.spec.mjs ├── columns.mjs ├── columns.spec.mjs ├── config.fixture.json ├── config.js ├── config.spec.mjs ├── depstore.js ├── distribute │ ├── export.mjs │ ├── export.spec.mjs │ ├── import.mjs │ ├── import.spec.mjs │ ├── log.mjs │ └── log.spec.mjs ├── env.js ├── env.spec.js ├── exit.js ├── exit.spec.js ├── fixture-user-menu │ ├── io-cp-fix.js │ ├── io-cp.js │ ├── io-mv-fix.js │ └── io-mv.js ├── fixture │ └── route.js ├── markdown │ ├── fixture │ │ ├── markdown.html │ │ ├── markdown.md │ │ └── markdown.zip │ ├── index.js │ ├── index.spec.mjs │ └── worker.js ├── modulas.js ├── prefixer.js ├── prefixer.spec.js ├── repl.js ├── rest │ ├── index.js │ ├── index.spec.js │ ├── info.js │ └── info.spec.js ├── root.js ├── root.spec.js ├── route.mjs ├── route.spec.mjs ├── server.mjs ├── show-config.js ├── template.js ├── terminal.js ├── terminal.spec.mjs ├── theme.mjs ├── themes.spec.mjs ├── user-menu.mjs ├── user-menu.spec.mjs ├── validate.mjs └── validate.spec.mjs ├── static ├── user-menu.js └── user-menu.spec.js ├── test ├── before.mjs ├── client │ └── listeners │ │ ├── get-index.js │ │ └── get-range.js ├── common │ ├── cloudfunc.html │ └── cloudfunc.js ├── fixture │ ├── copy.txt │ ├── empty-file │ ├── pack │ ├── pack.tar.gz │ └── pack.zip ├── rest │ ├── config.mjs │ ├── copy.mjs │ ├── fs.mjs │ ├── move.mjs │ ├── pack.mjs │ └── rename.mjs ├── server │ ├── console.mjs │ ├── env.js │ ├── modulas.mjs │ └── show-config.js └── static.mjs ├── tmpl ├── config.hbs ├── fs │ ├── file.hbs │ ├── link.hbs │ ├── panel.hbs │ ├── path.hbs │ └── pathLink.hbs ├── upload.hbs └── view │ └── media.hbs └── webpack.config.js /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "module:babel-plugin-macros", 7 | "@babel/plugin-transform-optional-chaining" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | last 2 Safari versions 3 | Firefox ESR 4 | not dead 5 | -------------------------------------------------------------------------------- /.cloudcmd.menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 'F2 - Rename file': async ({DOM}) => { 5 | await DOM.renameCurrent(); 6 | }, 7 | 'L - Lint': async ({CloudCmd}) => { 8 | const {TerminalRun} = CloudCmd; 9 | await run(TerminalRun, 'npm run lint'); 10 | }, 11 | 'F - Fix Lint': async ({CloudCmd}) => { 12 | const {TerminalRun} = CloudCmd; 13 | await run(TerminalRun, 'npm run fix:lint'); 14 | }, 15 | 'T - Test': async ({CloudCmd}) => { 16 | const {TerminalRun} = CloudCmd; 17 | 18 | await run(TerminalRun, 'npm run test'); 19 | }, 20 | 'C - Coverage': async ({CloudCmd}) => { 21 | const {TerminalRun} = CloudCmd; 22 | 23 | await run(TerminalRun, 'npm run coverage'); 24 | }, 25 | 'D - Build Dev': async ({CloudCmd}) => { 26 | const {TerminalRun} = CloudCmd; 27 | 28 | await run(TerminalRun, 'npm run build:client:dev'); 29 | CloudCmd.refresh(); 30 | }, 31 | 'P - Build Prod': async ({CloudCmd}) => { 32 | const {TerminalRun} = CloudCmd; 33 | 34 | await run(TerminalRun, 'npm run build:client'); 35 | CloudCmd.refresh(); 36 | }, 37 | }; 38 | 39 | async function run(TerminalRun, command) { 40 | await TerminalRun.show({ 41 | command, 42 | closeMessage: 'Press any key to close Terminal', 43 | autoClose: false, 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | node_modules 4 | npm-debug.log* 5 | coverage 6 | test 7 | manifest.yml 8 | app.json 9 | bower.json 10 | yarn-error.log 11 | yarn.lock 12 | now.json 13 | 14 | docker 15 | 16 | webpack.config.js 17 | cssnano.config.js 18 | 19 | bin/release.js 20 | 21 | client 22 | server_ 23 | 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # http://editorconfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = false 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: [ 5 | 'plugin:putout/safe+align', 6 | ], 7 | plugins: [ 8 | 'putout', 9 | 'n', 10 | ], 11 | rules: { 12 | 'key-spacing': 'off', 13 | 'n/prefer-node-protocol': 'error', 14 | }, 15 | overrides: [{ 16 | files: ['bin/release.js'], 17 | rules: { 18 | 'no-console': 'off', 19 | 'n/shebang': 'off', 20 | }, 21 | }, { 22 | files: ['client/dom/index.js'], 23 | rules: { 24 | 'no-multi-spaces': 'off', 25 | }, 26 | }, { 27 | files: ['bin/cloudcmd.js'], 28 | rules: { 29 | 'no-console': 'off', 30 | }, 31 | }, { 32 | files: ['{client,common,static}/**/*.js'], 33 | env: { 34 | browser: true, 35 | }, 36 | }], 37 | ignorePatterns: ['*.md{js}'], 38 | }; 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: coderaiser 2 | open_collective: cloudcmd 3 | ko_fi: coderaiser 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Bug report 4 | about: Create a report to help us improve 5 | title: '' 6 | labels: needs clarification 7 | assignees: coderaiser 8 | 9 | --- 10 | 11 | **Describe the bug** 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Desktop (please complete the following information):** 29 | 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | - Node.js version 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Stack Overflow 4 | url: https://stackoverflow.com/search?q=cloudcmd 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Feature request 4 | about: Suggest an idea for this project 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | *** 2 | 3 | name: Tracking issue 4 | about: Create an issue with bug report or feature request. 5 | title: "" 6 | labels: needs triage 7 | assignees: coderaiser 8 | 9 | *** 10 | 11 | - **Version** (`cloudcmd -v`): 12 | - **Node Version** `node -v`: 13 | - **OS** (`uname -a` on Linux): 14 | - **Browser name/version**: 15 | - **Used Command Line Parameters**: 16 | - **Changed Config**: 17 | 18 | ```json 19 | {} 20 | ``` 21 | 22 | - [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd** 23 | - [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser** 24 | - [ ] 💪 **I'm willing to work on this issue** 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | - [ ] commit message named according to [Contributing Guide](https://github.com/coderaiser/cloudcmd/blob/master/CONTRIBUTING.md "Contributting Guide") 7 | - [ ] `npm run fix:lint` is OK 8 | - [ ] `npm test` is OK 9 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker CI 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | buildx: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | packages: write 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - uses: oven-sh/setup-bun@v1 16 | with: 17 | bun-version: latest 18 | - name: Use Node.js 22.x 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 22.x 22 | - name: Install Redrun 23 | run: bun i redrun -g --no-save 24 | - name: NPM Install 25 | run: bun i --no-save 26 | - name: Lint 27 | run: redrun lint 28 | - name: Build 29 | id: build 30 | run: | 31 | redrun build 32 | echo "::set-output name=version::$(grep '"version":' package.json -m1 | cut -d\" -f4)" 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | - name: Set up Docker Buildx 36 | uses: docker/setup-buildx-action@v3 37 | - name: Login to DockerHub 38 | uses: docker/login-action@v3 39 | with: 40 | username: ${{ secrets.DOCKER_USERNAME }} 41 | password: ${{ secrets.DOCKER_TOKEN }} 42 | - name: Login to GitHub Container Registry 43 | uses: docker/login-action@v3 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.actor }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | - name: Build and push base-image 49 | uses: docker/build-push-action@v5 50 | with: 51 | context: . 52 | file: docker/Dockerfile 53 | platforms: linux/amd64,linux/arm64 54 | push: true 55 | tags: | 56 | coderaiser/cloudcmd:latest 57 | coderaiser/cloudcmd:${{ steps.build.outputs.version }} 58 | ghcr.io/${{ github.repository }}:latest 59 | ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }} 60 | - name: Build and push alpine-image 61 | uses: docker/build-push-action@v5 62 | with: 63 | context: . 64 | file: docker/Dockerfile.alpine 65 | platforms: linux/amd64,linux/arm64 66 | push: true 67 | tags: | 68 | coderaiser/cloudcmd:latest-alpine 69 | coderaiser/cloudcmd:${{ steps.build.outputs.version }}-alpine 70 | ghcr.io/${{ github.repository }}:latest-alpine 71 | ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-alpine 72 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: 3 | - push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | env: 8 | NAME: cloudcmd 9 | strategy: 10 | matrix: 11 | node-version: 12 | - 20.x 13 | - 22.x 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: oven-sh/setup-bun@v1 17 | with: 18 | bun-version: latest 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install Redrun 24 | run: bun i redrun -g --no-save 25 | - name: Install 26 | run: bun i --no-save 27 | - name: Lint 28 | run: redrun fix:lint 29 | - uses: actions/cache@v4 30 | with: 31 | path: | 32 | ~/.cargo/bin/ 33 | ~/.cargo/registry/index/ 34 | ~/.cargo/registry/cache/ 35 | ~/.cargo/git/db/ 36 | target/ 37 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 38 | - name: Typos Install 39 | run: cargo install typos-cli || echo 'already installed' 40 | - name: Typos 41 | run: typos --write-changes 42 | - name: Commit fixes 43 | uses: EndBug/add-and-commit@v9 44 | continue-on-error: true 45 | with: 46 | message: "chore: ${{ env.NAME }}: actions: lint ☘️" 47 | - name: Build 48 | run: redrun build 49 | - name: Test 50 | run: redrun test 51 | - name: Coverage 52 | run: redrun coverage coverage:report 53 | - name: Coveralls 54 | uses: coverallsapp/github-action@v2 55 | continue-on-error: true 56 | with: 57 | github-token: ${{ secrets.GITHUB_TOKEN }} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | yarn.lock 3 | yarn-error.log 4 | node_modules 5 | npm-debug.log* 6 | coverage 7 | 8 | modules/execon 9 | modules/emitify 10 | 11 | .nyc_output 12 | 13 | *.swp 14 | .DS_Store 15 | 16 | dist 17 | dist-dev 18 | 19 | .idea 20 | -------------------------------------------------------------------------------- /.madrun.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {run, cutEnv} from 'madrun'; 3 | 4 | const testEnv = { 5 | THREAD_IT_COUNT: 0, 6 | SUPERTAPE_TIMEOUT: 7000, 7 | }; 8 | 9 | const is17 = /^v1[789]/.test(process.version); 10 | const is20 = process.version.startsWith('v2'); 11 | 12 | // fix for ERR_OSSL_EVP_UNSUPPORTED on node v17 13 | // flag '--openssl-legacy-provider' not supported 14 | // on earlier version of node.js 15 | // 16 | // https://stackoverflow.com/a/69746937/4536327 17 | const buildEnv = (is17 || is20) && { 18 | NODE_OPTIONS: '--openssl-legacy-provider', 19 | }; 20 | 21 | export default { 22 | 'start': () => 'node bin/cloudcmd.mjs', 23 | 'start:dev': async () => await run('start', null, { 24 | NODE_ENV: 'development', 25 | }), 26 | 'build:start': () => run(['build:client', 'start']), 27 | 'build:start:dev': () => run(['build:client:dev', 'start:dev']), 28 | 'lint:all': () => run('lint:progress'), 29 | 'lint': () => 'putout .', 30 | 'lint:progress': () => run('lint', '-f progress'), 31 | 'watch:lint': () => 'nodemon -w client -w server -w test -w common -w .webpack -x "putout -s"', 32 | 'fresh:lint': () => run('lint', '--fresh'), 33 | 'lint:fresh': () => run('lint', '--fresh'), 34 | 'fix:lint': () => run('lint', '--fix'), 35 | 'lint:stream': () => run('lint', '-f stream'), 36 | 'test': () => [testEnv, `tape 'test/**/*.{js,mjs}' '{client,static,common,server}/**/*.spec.{js,mjs}' -f fail`], 37 | 'test:client': () => `tape 'test/client/**/*.js'`, 38 | 'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`, 39 | 'wisdom': () => run(['lint:all', 'build', 'test']), 40 | 'wisdom:type': () => 'bin/release.mjs', 41 | 'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`], 42 | 'coverage:report': () => 'c8 report --reporter=lcov', 43 | 'report': () => 'c8 report --reporter=lcov', 44 | '6to5': () => [buildEnv, 'webpack --progress'], 45 | '6to5:client': () => run('6to5', '--mode production'), 46 | '6to5:client:dev': async () => await run('6to5', '--mode development', { 47 | NODE_ENV: 'development', 48 | }), 49 | 'pre6to5:client': () => 'rimraf dist', 50 | 'pre6to5:client:dev': () => 'rimraf dist-dev', 51 | 'watch:client': () => run('6to5:client', '--watch'), 52 | 'watch:client:dev': () => run('6to5:client:dev', '--watch'), 53 | 'watch:server': () => 'nodemon bin/cloudcmd.js', 54 | 'watch:test': async () => [testEnv, `nodemon -w client -w server -w test -w common -x ${await cutEnv('test')}`], 55 | 'watch:test:client': async () => `nodemon -w client -w test/client -x ${await run('test:client')}`, 56 | 'watch:test:server': async () => `nodemon -w client -w test/client -x ${await run('test:server')}`, 57 | 'watch:coverage': async () => [testEnv, `nodemon -w server -w test -w common -x ${await cutEnv('coverage')}`], 58 | 'build': async () => run('6to5:*'), 59 | 'build:dev': async () => run('build:client:dev'), 60 | 'build:client': () => run('6to5:client'), 61 | 'build:client:dev': () => run('6to5:client:dev'), 62 | 'heroku-postbuild': () => run('6to5:client'), 63 | }; 64 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | *.fixture.js* 4 | manifest.yml 5 | docker 6 | docker-compose.yml 7 | test 8 | fixture 9 | fixture-* 10 | coverage 11 | css 12 | html 13 | yarn-error.log 14 | yarn.lock 15 | now.json 16 | cssnano.config.js 17 | 18 | app.json 19 | bower.json 20 | manifest.yml 21 | 22 | bin/release.js 23 | 24 | client 25 | img/logo/cloudcmd-hq.png 26 | 27 | webpack.config.js 28 | 29 | *.ai 30 | *.cdr 31 | *.eps 32 | 33 | *.config.* 34 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.15.1 2 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": false, 3 | "all": false, 4 | "exclude": [ 5 | "**/*.spec.js", 6 | "**/fixture", 7 | "**/*.*.js", 8 | "**/*.config.*", 9 | "**/test/**" 10 | ], 11 | "branches": 100, 12 | "lines": 100, 13 | "functions": 100, 14 | "statements": 100 15 | } 16 | -------------------------------------------------------------------------------- /.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["cloudcmd"], 3 | "ignore": [ 4 | "html", 5 | "fixture*", 6 | "app.json", 7 | "fontello.json" 8 | ], 9 | "rules": { 10 | "github/set-node-versions": "off" 11 | }, 12 | "match": { 13 | "base64": { 14 | "types/convert-typeof-to-is-type": "off" 15 | }, 16 | "*.md": { 17 | "nodejs/convert-commonjs-to-esm": "on" 18 | }, 19 | ".webpack": { 20 | "webpack": "on" 21 | }, 22 | "server": { 23 | "nodejs/remove-process-exit": "on" 24 | }, 25 | "server/{server,exit}.js": { 26 | "nodejs/remove-process-exit": "off" 27 | }, 28 | "server/{server,exit,terminal,distribute/log}.{js,mjs}": { 29 | "remove-console": "off" 30 | }, 31 | "client/{client,cloudcmd,load-module}.js": { 32 | "remove-console": "off" 33 | }, 34 | "client/sw": { 35 | "remove-console": "off" 36 | }, 37 | "test/common/cloudfunc.js": { 38 | "remove-console": "off" 39 | }, 40 | "storage.js": { 41 | "promises/remove-useless-async": "off" 42 | }, 43 | "docker.yml": { 44 | "github/set-node-versions": "off" 45 | }, 46 | "vim.js": { 47 | "merge-duplicate-functions": "off" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude= ["ChangeLog", "*.js"] 3 | -------------------------------------------------------------------------------- /.webpack/css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {env} = require('node:process'); 4 | const fs = require('node:fs'); 5 | const { 6 | basename, 7 | extname, 8 | join, 9 | } = require('node:path'); 10 | 11 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 12 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 13 | 14 | const isDev = env.NODE_ENV === 'development'; 15 | 16 | const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`); 17 | const extractMain = extractCSS('[name]'); 18 | 19 | const cssNames = [ 20 | 'nojs', 21 | 'view', 22 | 'config', 23 | 'terminal', 24 | 'user-menu', 25 | ...getCSSList('columns'), 26 | ...getCSSList('themes'), 27 | ]; 28 | 29 | const cssPlugins = cssNames.map(extractCSS); 30 | const clean = (a) => a.filter(Boolean); 31 | 32 | const plugins = clean([ 33 | ...cssPlugins, 34 | extractMain, 35 | !isDev && new OptimizeCssAssetsPlugin(), 36 | ]); 37 | 38 | const rules = [{ 39 | test: /\.css$/, 40 | exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*|themes.*)\.css/, 41 | use: extractMain.extract(['css-loader']), 42 | }, ...cssPlugins.map(extract), { 43 | test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/, 44 | use: { 45 | loader: 'url-loader', 46 | options: { 47 | limit: 100_000, 48 | }, 49 | }, 50 | }]; 51 | 52 | module.exports = { 53 | plugins, 54 | module: { 55 | rules, 56 | }, 57 | }; 58 | 59 | function getCSSList(dir) { 60 | const base = (a) => basename(a, extname(a)); 61 | const addDir = (name) => `${dir}/${name}`; 62 | const rootDir = join(__dirname, '..'); 63 | 64 | return fs 65 | .readdirSync(`${rootDir}/css/${dir}`) 66 | .map(base) 67 | .map(addDir); 68 | } 69 | 70 | function extract(extractPlugin) { 71 | const {filename} = extractPlugin; 72 | 73 | return { 74 | test: RegExp(`css/${filename}`), 75 | use: extractPlugin.extract(['css-loader']), 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /.webpack/html.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {env} = require('node:process'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const isDev = env.NODE_ENV === 'development'; 7 | 8 | const plugins = [ 9 | new HtmlWebpackPlugin({ 10 | inject: false, 11 | template: 'html/index.html', 12 | minify: !isDev && getMinifyHtmlOptions(), 13 | }), 14 | ]; 15 | 16 | module.exports = { 17 | plugins, 18 | }; 19 | 20 | function getMinifyHtmlOptions() { 21 | return { 22 | removeComments: true, 23 | removeCommentsFromCDATA: true, 24 | removeCDATASectionsFromCDATA: true, 25 | collapseWhitespace: true, 26 | collapseBooleanAttributes: true, 27 | removeAttributeQuotes: true, 28 | removeRedundantAttributes: true, 29 | useShortDoctype: true, 30 | removeEmptyAttributes: true, 31 | /* оставляем, поскольку у нас 32 | * в элемент fm генерируеться 33 | * таблица файлов 34 | */ 35 | removeEmptyElements: false, 36 | removeOptionalTags: true, 37 | removeScriptTypeAttributes: true, 38 | removeStyleLinkTypeAttributes: true, 39 | 40 | minifyJS: true, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /.yaspellerrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreDigits": true, 3 | "excludeFiles": [ 4 | ".git", 5 | "modules", 6 | "lib", 7 | "node_modules" 8 | ], 9 | "fileExtensions": [ 10 | ".md" 11 | ], 12 | "dictionary":[ 13 | "CloudCmd", 14 | "Dev", 15 | "Dropbox", 16 | "Deepword", 17 | "Dword", 18 | "FilePicker", 19 | "GDrive", 20 | "Github", 21 | "Heroku", 22 | "Iptables", 23 | "JitSu", 24 | "Node", 25 | "IO", 26 | "Olena", 27 | "TarZak", 28 | "Termux", 29 | "Zalitok", 30 | "WebSocket", 31 | "auth", 32 | "autostart", 33 | "binded", 34 | "cd", 35 | "cloudcmd", 36 | "coderaiser", 37 | "com", 38 | "deepword", 39 | "dev", 40 | "destructuring", 41 | "dropbox", 42 | "dword", 43 | "edward", 44 | "favicon", 45 | "github", 46 | "gz", 47 | "io", 48 | "js", 49 | "linux", 50 | "maintainers", 51 | "markdown", 52 | "microservice", 53 | "minification", 54 | "mouseup", 55 | "named", 56 | "nginx", 57 | "npm", 58 | "or io", 59 | "patreon", 60 | "rc", 61 | "refactor", 62 | "sexualized", 63 | "sslPort", 64 | "unselect", 65 | "util", 66 | "v0", 67 | "v1", 68 | "v2", 69 | "yml", 70 | "systemd" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Commit 2 | 3 | Format of the commit message: **type(scope) subject** 4 | 5 | **Type**: 6 | 7 | - feature: scope: subject 8 | - fix: scope: subject 9 | - docs: scope: subject 10 | - refactor: scope: subject 11 | - test: scope: subject 12 | - chore: scope: subject 13 | 14 | **Scope**: 15 | Scope could be anything specifying place of the commit change. 16 | For example util, console, view, edit, style etc... 17 | 18 | **Subject text**: 19 | 20 | - use imperative, present tense: “change” not “changed” nor “changes” 21 | - don't capitalize first letter 22 | - no dot (.) at the end 23 | **Message body**: 24 | - just as in use imperative, present tense: “change” not “changed” nor “changes” 25 | - includes motivation for the change and contrasts with previous behavior 26 | 27 | **Examples**: 28 | 29 | - [fix: style: .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050) 30 | - [fix: console: dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012-2025 Coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /bin/release.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {promisify} from 'node:util'; 4 | import process from 'node:process'; 5 | import tryToCatch from 'try-to-catch'; 6 | import {createSimport} from 'simport'; 7 | import minor from 'minor'; 8 | import _place from 'place'; 9 | import rendy from 'rendy'; 10 | import shortdate from 'shortdate'; 11 | 12 | const simport = createSimport(import.meta.url); 13 | const place = promisify(_place); 14 | 15 | const Info = await simport('../package.json'); 16 | 17 | await main(); 18 | 19 | async function main() { 20 | const history = '## Version history\n\n'; 21 | const link = '//github.com/coderaiser/cloudcmd/releases/tag/'; 22 | const template = 23 | '- *{{ date }}*, ' + 24 | '**[v{{ version }}]' + 25 | '(' + link + 26 | 'v{{ version }})**\n'; 27 | 28 | const {version} = Info; 29 | 30 | const [error, versionNew] = await tryToCatch(cl); 31 | 32 | if (error) 33 | return console.error(error); 34 | 35 | await replaceVersion('README.md', version, versionNew); 36 | await replaceVersion('HELP.md', version, versionNew); 37 | 38 | const historyNew = history + rendy(template, { 39 | date: shortdate(), 40 | version: versionNew, 41 | }); 42 | 43 | await replaceVersion('HELP.md', history, historyNew); 44 | } 45 | 46 | async function replaceVersion(name, version, versionNew) { 47 | const [error] = await tryToCatch(place, name, version, versionNew); 48 | 49 | if (error) 50 | return console.error(error); 51 | 52 | console.log(`done: ${name}`); 53 | } 54 | 55 | async function cl() { 56 | const {argv} = process; 57 | const length = argv.length - 1; 58 | const last = process.argv[length]; 59 | const regExp = /^--(major|minor|patch)$/; 60 | const [, match] = last.match(regExp) || []; 61 | 62 | console.log(last); 63 | 64 | if (!regExp.test(last)) 65 | throw Error('ERROR: version is missing. release --patch|--minor|--major'); 66 | 67 | return getVersionNew(last, match); 68 | } 69 | 70 | function getVersionNew(last, match) { 71 | if (match) 72 | return minor(match, Info.version); 73 | 74 | return last.substr(3); 75 | } 76 | -------------------------------------------------------------------------------- /client/cloudcmd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | require('./css'); 5 | 6 | const wraptile = require('wraptile'); 7 | const load = require('load.js'); 8 | 9 | const {registerSW, listenSW} = require('./sw/register'); 10 | 11 | const isDev = process.env.NODE_ENV === 'development'; 12 | 13 | module.exports = async (config) => { 14 | window.Util = require('../common/util'); 15 | window.CloudFunc = require('../common/cloudfunc'); 16 | 17 | window.DOM = require('./dom'); 18 | window.CloudCmd = require('./client'); 19 | 20 | await register(config); 21 | 22 | require('./listeners'); 23 | require('./key'); 24 | require('./sort'); 25 | 26 | const prefix = getPrefix(config.prefix); 27 | 28 | window.CloudCmd.init(prefix, config); 29 | }; 30 | window.CloudCmd = module.exports; 31 | 32 | function getPrefix(prefix) { 33 | if (!prefix) 34 | return ''; 35 | 36 | if (!prefix.indexOf('/')) 37 | return prefix; 38 | 39 | return `/${prefix}`; 40 | } 41 | 42 | const onUpdateFound = wraptile(async (config) => { 43 | if (isDev) 44 | return; 45 | 46 | const {DOM} = window; 47 | const prefix = getPrefix(config.prefix); 48 | 49 | await load.js(`${prefix}/dist/cloudcmd.common.js`); 50 | await load.js(`${prefix}/dist/cloudcmd.js`); 51 | 52 | console.log('cloudcmd: sw: updated'); 53 | 54 | DOM.Events.removeAll(); 55 | window.CloudCmd(config); 56 | }); 57 | 58 | async function register(config) { 59 | const {prefix} = config; 60 | const sw = await registerSW(prefix); 61 | 62 | listenSW(sw, 'updatefound', onUpdateFound(config)); 63 | } 64 | -------------------------------------------------------------------------------- /client/css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../css/main.css'); 4 | require('../css/nojs.css'); 5 | require('../css/columns/name-size-date.css'); 6 | require('../css/columns/name-size.css'); 7 | require('../css/themes/light.css'); 8 | require('../css/themes/dark.css'); 9 | -------------------------------------------------------------------------------- /client/dom/dialog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tryToCatch = require('try-to-catch'); 4 | 5 | const { 6 | alert, 7 | prompt, 8 | confirm, 9 | progress, 10 | } = require('smalltalk'); 11 | 12 | const title = 'Cloud Commander'; 13 | 14 | module.exports.alert = (...a) => alert(title, ...a, { 15 | cancel: false, 16 | }); 17 | 18 | module.exports.prompt = (...a) => tryToCatch(prompt, title, ...a); 19 | module.exports.confirm = (...a) => tryToCatch(confirm, title, ...a); 20 | module.exports.progress = (...a) => progress(title, ...a); 21 | 22 | module.exports.alert.noFiles = () => { 23 | return alert(title, 'No files selected!', { 24 | cancel: false, 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /client/dom/directory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | const philip = require('philip'); 5 | 6 | const Images = require('./images'); 7 | const {FS} = require('../../common/cloudfunc'); 8 | const DOM = require('.'); 9 | const Dialog = require('./dialog'); 10 | 11 | const {getCurrentDirPath: getPathWhenRootEmpty} = DOM; 12 | 13 | module.exports = (items) => { 14 | if (items.length) 15 | Images.show('top'); 16 | 17 | const entries = Array 18 | .from(items) 19 | .map((item) => item.webkitGetAsEntry()); 20 | 21 | const dirPath = getPathWhenRootEmpty(); 22 | const path = dirPath.replace(/\/$/, ''); 23 | 24 | const progress = Dialog.progress('Uploading...'); 25 | 26 | progress.catch(() => { 27 | Dialog.alert('Upload aborted'); 28 | uploader.abort(); 29 | }); 30 | 31 | const uploader = philip(entries, (type, name, data, i, n, callback) => { 32 | const {prefixURL} = CloudCmd; 33 | const full = prefixURL + FS + path + name; 34 | 35 | let upload; 36 | switch(type) { 37 | case 'file': 38 | upload = uploadFile(full, data); 39 | break; 40 | 41 | case 'directory': 42 | upload = uploadDir(full); 43 | break; 44 | } 45 | 46 | upload.on('end', callback); 47 | 48 | upload.on('progress', (count) => { 49 | const current = percent(i, n); 50 | const next = percent(i + 1, n); 51 | const max = next - current; 52 | const value = current + percent(count, 100, max); 53 | 54 | progress.setProgress(value); 55 | }); 56 | }); 57 | 58 | uploader.on('error', (error) => { 59 | Dialog.alert(error); 60 | uploader.abort(); 61 | }); 62 | 63 | uploader.on('end', CloudCmd.refresh); 64 | }; 65 | 66 | const percent = (i, n, per = 100) => Math.round(i * per / n); 67 | 68 | const uploadFile = (url, data) => DOM.load.put(url, data); 69 | 70 | const uploadDir = (url) => DOM.load.put(`${url}?dir`); 71 | -------------------------------------------------------------------------------- /client/dom/dom-tree.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const currify = require('currify'); 4 | 5 | const DOM = module.exports; 6 | 7 | /** 8 | * check class of element 9 | * 10 | * @param element 11 | * @param className 12 | */ 13 | const isContainClass = (element, className) => { 14 | if (!element) 15 | throw Error('element could not be empty!'); 16 | 17 | if (!className) 18 | throw Error('className could not be empty!'); 19 | 20 | if (Array.isArray(className)) 21 | return className.some(currify( 22 | isContainClass, 23 | element, 24 | )); 25 | 26 | const {classList} = element; 27 | 28 | return classList.contains(className); 29 | }; 30 | 31 | module.exports.isContainClass = isContainClass; 32 | /** 33 | * Function search element by tag 34 | * @param tag - className 35 | * @param element - element 36 | */ 37 | module.exports.getByTag = (tag, element = document) => { 38 | return element.getElementsByTagName(tag); 39 | }; 40 | 41 | /** 42 | * Function search element by id 43 | * @param Id - id 44 | */ 45 | module.exports.getById = (id, element = document) => { 46 | return element.querySelector(`#${id}`); 47 | }; 48 | 49 | /** 50 | * Function search first element by class name 51 | * @param className - className 52 | * @param element - element 53 | */ 54 | module.exports.getByClass = (className, element = document) => DOM.getByClassAll(className, element)[0]; 55 | 56 | module.exports.getByDataName = (attribute, element = document) => { 57 | const selector = '[' + 'data-name="' + attribute + '"]'; 58 | return element.querySelector(selector); 59 | }; 60 | 61 | /** 62 | * Function search element by class name 63 | * @param pClass - className 64 | * @param element - element 65 | */ 66 | module.exports.getByClassAll = (className, element) => { 67 | return (element || document).getElementsByClassName(className); 68 | }; 69 | 70 | /** 71 | * add class=hidden to element 72 | * 73 | * @param element 74 | */ 75 | module.exports.hide = (element) => { 76 | element.classList.add('hidden'); 77 | return DOM; 78 | }; 79 | 80 | module.exports.show = (element) => { 81 | element.classList.remove('hidden'); 82 | return DOM; 83 | }; 84 | -------------------------------------------------------------------------------- /client/dom/dom-tree.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const {create} = require('auto-globals'); 5 | const tryCatch = require('try-catch'); 6 | 7 | const {isContainClass} = require('./dom-tree'); 8 | 9 | test('dom: isContainClass: no element', (t) => { 10 | const [e] = tryCatch(isContainClass); 11 | 12 | t.equal(e.message, 'element could not be empty!', 'should throw when no element'); 13 | t.end(); 14 | }); 15 | 16 | test('dom: isContainClass: no className', (t) => { 17 | const [e] = tryCatch(isContainClass, {}); 18 | 19 | t.equal(e.message, 'className could not be empty!', 'should throw when no element'); 20 | t.end(); 21 | }); 22 | 23 | test('dom: isContainClass: contains', (t) => { 24 | const el = create(); 25 | const {contains} = el.classList; 26 | 27 | const className = 'hello'; 28 | isContainClass(el, className); 29 | 30 | t.calledWith(contains, [className], 'should call contains'); 31 | t.end(); 32 | }); 33 | 34 | test('dom: isContainClass: contains: array', (t) => { 35 | const el = create(); 36 | const {contains} = el.classList; 37 | 38 | const className = 'hello'; 39 | isContainClass(el, ['world', className, 'hello']); 40 | 41 | t.calledWith(contains, [className], 'should call contains'); 42 | t.end(); 43 | }); 44 | -------------------------------------------------------------------------------- /client/dom/events/event-store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let list = []; 4 | 5 | module.exports.add = (el, name, fn) => { 6 | list.push([ 7 | el, 8 | name, 9 | fn, 10 | ]); 11 | }; 12 | 13 | module.exports.clear = () => { 14 | list = []; 15 | }; 16 | 17 | module.exports.get = () => list; 18 | -------------------------------------------------------------------------------- /client/dom/events/event-store.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const eventStore = require('./event-store'); 5 | 6 | test('event-store: get', (t) => { 7 | const el = {}; 8 | const name = 'click'; 9 | const fn = () => {}; 10 | 11 | eventStore.add(el, name, fn); 12 | const result = eventStore.get(); 13 | 14 | const expected = [ 15 | [ 16 | el, 17 | name, 18 | fn, 19 | ], 20 | ]; 21 | 22 | t.deepEqual(result, expected); 23 | t.end(); 24 | }); 25 | 26 | test('event-store: clear', (t) => { 27 | const el = {}; 28 | const name = 'click'; 29 | const fn = () => {}; 30 | 31 | eventStore.add(el, name, fn); 32 | eventStore.clear(); 33 | 34 | const result = eventStore.get(); 35 | const expected = []; 36 | 37 | t.deepEqual(result, expected); 38 | t.end(); 39 | }); 40 | -------------------------------------------------------------------------------- /client/dom/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('css-modules-require-hook/preset'); 4 | 5 | const {test, stub} = require('supertape'); 6 | const mockRequire = require('mock-require'); 7 | const {getCSSVar} = require('./index'); 8 | const {reRequire, stopAll} = mockRequire; 9 | 10 | global.CloudCmd = {}; 11 | 12 | test('cloudcmd: client: dom: goToDirectory', async (t) => { 13 | const path = ''; 14 | const {CloudCmd} = global; 15 | const changeDir = stub(); 16 | const prompt = stub().returns([null, path]); 17 | 18 | CloudCmd.changeDir = changeDir; 19 | 20 | mockRequire('./dialog', { 21 | prompt, 22 | }); 23 | 24 | const {goToDirectory} = reRequire('.'); 25 | 26 | await goToDirectory(); 27 | 28 | stopAll(); 29 | 30 | t.calledWith(changeDir, [path]); 31 | t.end(); 32 | }); 33 | 34 | test('cloudcmd: client: dom: getCSSVar', (t) => { 35 | const body = {}; 36 | const getPropertyValue = stub().returns(0); 37 | 38 | global.getComputedStyle = stub().returns({ 39 | getPropertyValue, 40 | }); 41 | const result = getCSSVar('hello', { 42 | body, 43 | }); 44 | 45 | delete global.getComputedStyle; 46 | 47 | t.notOk(result); 48 | t.end(); 49 | }); 50 | 51 | test('cloudcmd: client: dom: getCSSVar: 1', (t) => { 52 | const body = {}; 53 | const getPropertyValue = stub().returns(1); 54 | 55 | global.getComputedStyle = stub().returns({ 56 | getPropertyValue, 57 | }); 58 | const result = getCSSVar('hello', { 59 | body, 60 | }); 61 | 62 | delete global.getComputedStyle; 63 | 64 | t.ok(result); 65 | t.end(); 66 | }); 67 | -------------------------------------------------------------------------------- /client/dom/io/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const mockRequire = require('mock-require'); 6 | 7 | const {reRequire, stopAll} = mockRequire; 8 | 9 | test('client: dom: io', (t) => { 10 | const sendRequest = stub(); 11 | mockRequire('./send-request', sendRequest); 12 | 13 | const io = reRequire('.'); 14 | 15 | io.createDirectory('/hello'); 16 | 17 | const expected = { 18 | imgPosition: { 19 | top: true, 20 | }, 21 | method: 'PUT', 22 | url: '/fs/hello?dir', 23 | }; 24 | 25 | stopAll(); 26 | 27 | t.calledWith(sendRequest, [expected]); 28 | t.end(); 29 | }); 30 | -------------------------------------------------------------------------------- /client/dom/io/send-request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | const {promisify} = require('es6-promisify'); 5 | 6 | const Images = require('../images'); 7 | const load = require('../load'); 8 | 9 | module.exports = promisify((params, callback) => { 10 | const p = params; 11 | const {prefixURL} = CloudCmd; 12 | 13 | p.url = prefixURL + p.url; 14 | p.url = encodeURI(p.url); 15 | 16 | p.url = replaceHash(p.url); 17 | 18 | load.ajax({ 19 | method: p.method, 20 | url: p.url, 21 | data: p.data, 22 | dataType: p.dataType, 23 | error: (jqXHR) => { 24 | const response = jqXHR.responseText; 25 | 26 | const {statusText, status} = jqXHR; 27 | 28 | const text = status === 404 ? response : statusText; 29 | 30 | callback(Error(text)); 31 | }, 32 | success: (data) => { 33 | Images.hide(); 34 | 35 | if (!p.notLog) 36 | CloudCmd.log(data); 37 | 38 | callback(null, data); 39 | }, 40 | }); 41 | }); 42 | 43 | module.exports._replaceHash = replaceHash; 44 | function replaceHash(url) { 45 | /* 46 | * if we send ajax request - 47 | * no need in hash so we escape # 48 | */ 49 | return url.replace(/#/g, '%23'); 50 | } 51 | -------------------------------------------------------------------------------- /client/dom/io/send-request.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const {_replaceHash} = require('./send-request'); 5 | 6 | test('cloudcmd: client: io: replaceHash', (t) => { 7 | const url = '/hello/####world'; 8 | const result = _replaceHash(url); 9 | const expected = '/hello/%23%23%23%23world'; 10 | 11 | t.equal(result, expected); 12 | t.end(); 13 | }); 14 | -------------------------------------------------------------------------------- /client/dom/load-remote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | const rendy = require('rendy'); 5 | const itype = require('itype'); 6 | const load = require('load.js'); 7 | const tryToCatch = require('try-to-catch'); 8 | 9 | const {findObjByNameInArr} = require('../../common/util'); 10 | 11 | const Files = require('./files'); 12 | 13 | module.exports = (name, options, callback = options) => { 14 | const {prefix, config} = CloudCmd; 15 | const o = options; 16 | 17 | if (o.name && window[o.name]) 18 | return callback(); 19 | 20 | Files.get('modules').then(async (modules) => { 21 | const online = config('online') && navigator.onLine; 22 | const module = findObjByNameInArr(modules.remote, name); 23 | 24 | const isArray = itype.array(module.local); 25 | const {version} = module; 26 | 27 | let remoteTmpls; 28 | let local; 29 | 30 | if (isArray) { 31 | remoteTmpls = module.remote; 32 | ({local} = module); 33 | } else { 34 | remoteTmpls = [module.remote]; 35 | local = [module.local]; 36 | } 37 | 38 | const localURL = local.map((url) => prefix + url); 39 | 40 | const remoteURL = remoteTmpls.map((tmpl) => { 41 | return rendy(tmpl, { 42 | version, 43 | }); 44 | }); 45 | 46 | if (online) { 47 | const [e] = await tryToCatch(load.parallel, remoteURL); 48 | 49 | if (!e) 50 | return callback(); 51 | } 52 | 53 | const [e] = await tryToCatch(load.parallel, localURL); 54 | callback(e); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /client/dom/operations/rename-current.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | const capitalize = require('just-capitalize'); 5 | 6 | const Dialog = require('../dialog'); 7 | const Storage = require('../storage'); 8 | const RESTful = require('../rest'); 9 | 10 | const { 11 | isCurrentFile, 12 | getCurrentName, 13 | getCurrentFile, 14 | getCurrentByName, 15 | getCurrentType, 16 | getCurrentDirPath, 17 | setCurrentName, 18 | } = require('../current-file'); 19 | 20 | module.exports = async (current) => { 21 | if (!isCurrentFile(current)) 22 | current = getCurrentFile(); 23 | 24 | const from = getCurrentName(current); 25 | 26 | if (from === '..') 27 | return Dialog.alert.noFiles(); 28 | 29 | const [cancel, to] = await Dialog.prompt('Rename', from); 30 | 31 | if (cancel) 32 | return; 33 | 34 | const nextFile = getCurrentByName(to); 35 | 36 | if (nextFile) { 37 | const type = getCurrentType(nextFile); 38 | const msg = `${capitalize(type)} "${to}" already exists. Proceed?`; 39 | const [cancel] = await Dialog.confirm(msg); 40 | 41 | if (cancel) 42 | return; 43 | } 44 | 45 | if (from === to) 46 | return; 47 | 48 | const dirPath = getCurrentDirPath(); 49 | 50 | const fromFull = `${dirPath}${from}`; 51 | const toFull = `${dirPath}${to}`; 52 | 53 | const [e] = await RESTful.rename(fromFull, toFull); 54 | 55 | if (e) 56 | return; 57 | 58 | setCurrentName(to, current); 59 | 60 | Storage.remove(dirPath); 61 | CloudCmd.refresh(); 62 | }; 63 | -------------------------------------------------------------------------------- /client/dom/operations/rename-current.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const mockRequire = require('mock-require'); 6 | 7 | const {reRequire, stopAll} = mockRequire; 8 | 9 | test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => { 10 | const current = {}; 11 | const isCurrentFile = stub(); 12 | 13 | mockRequire('../dialog', stubDialog()); 14 | mockRequire('../current-file', stubCurrentFile({ 15 | isCurrentFile, 16 | })); 17 | 18 | const renameCurrent = reRequire('./rename-current'); 19 | await renameCurrent(current); 20 | 21 | stopAll(); 22 | 23 | t.calledWith(isCurrentFile, [current], 'should call isCurrentFile'); 24 | t.end(); 25 | }); 26 | 27 | test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => { 28 | const current = {}; 29 | const name = 'hello'; 30 | const {CloudCmd} = global; 31 | 32 | global.CloudCmd = { 33 | refresh: stub(), 34 | }; 35 | 36 | const prompt = stub().returns([null, name]); 37 | const confirm = stub().returns([true]); 38 | 39 | const getCurrentByName = stub().returns(current); 40 | const getCurrentType = stub().returns('directory'); 41 | 42 | mockRequire('../dialog', stubDialog({ 43 | confirm, 44 | prompt, 45 | })); 46 | 47 | mockRequire('../current-file', stubCurrentFile({ 48 | getCurrentByName, 49 | getCurrentType, 50 | })); 51 | 52 | const renameCurrent = reRequire('./rename-current'); 53 | await renameCurrent(); 54 | 55 | const expected = 'Directory "hello" already exists. Proceed?'; 56 | 57 | global.CloudCmd = CloudCmd; 58 | 59 | stopAll(); 60 | 61 | t.calledWith(confirm, [expected], 'should call confirm'); 62 | t.end(); 63 | }); 64 | 65 | const stubDialog = (fns = {}) => { 66 | const { 67 | alert = stub().returns([]), 68 | confirm = stub().returns([]), 69 | prompt = stub().returns([]), 70 | } = fns; 71 | 72 | return { 73 | alert, 74 | confirm, 75 | prompt, 76 | }; 77 | }; 78 | 79 | const stubCurrentFile = (fns = {}) => { 80 | const { 81 | isCurrentFile = stub(), 82 | getCurrentName = stub(), 83 | getCurrentFile = stub(), 84 | getCurrentByName = stub(), 85 | getCurrentType = stub(), 86 | getCurrentDirPath = stub(), 87 | setCurrentName = stub(), 88 | } = fns; 89 | 90 | return { 91 | isCurrentFile, 92 | getCurrentName, 93 | getCurrentFile, 94 | getCurrentByName, 95 | getCurrentType, 96 | getCurrentDirPath, 97 | setCurrentName, 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /client/dom/rest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tryToCatch = require('try-to-catch'); 4 | 5 | const {encode} = require('../../common/entity'); 6 | 7 | const Images = require('./images'); 8 | const IO = require('./io'); 9 | const Dialog = require('./dialog'); 10 | 11 | const handleError = (promise) => async (...args) => { 12 | const [e, data] = await tryToCatch(promise, ...args); 13 | 14 | if (!e) 15 | return [e, data]; 16 | 17 | const encoded = encode(e.message); 18 | 19 | Images.show.error(encoded); 20 | Dialog.alert(encoded); 21 | 22 | return [e, data]; 23 | }; 24 | 25 | module.exports.delete = handleError(IO.delete); 26 | module.exports.patch = handleError(IO.patch); 27 | module.exports.write = handleError(IO.write); 28 | module.exports.createDirectory = handleError(IO.createDirectory); 29 | module.exports.read = handleError(IO.read); 30 | module.exports.copy = handleError(IO.copy); 31 | module.exports.pack = handleError(IO.pack); 32 | module.exports.extract = handleError(IO.extract); 33 | module.exports.move = handleError(IO.move); 34 | module.exports.rename = handleError(IO.rename); 35 | 36 | module.exports.Config = { 37 | read: handleError(IO.Config.read), 38 | write: handleError(IO.Config.write), 39 | }; 40 | 41 | module.exports.Markdown = { 42 | read: handleError(IO.Markdown.read), 43 | render: handleError(IO.Markdown.render), 44 | }; 45 | -------------------------------------------------------------------------------- /client/dom/select-by-pattern.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let SelectType = '*.*'; 4 | 5 | const {getRegExp} = require('../../common/util'); 6 | const {alert, prompt} = require('./dialog'); 7 | 8 | const DOM = require('.'); 9 | 10 | module.exports = async (msg, files) => { 11 | if (!files) 12 | return; 13 | 14 | const allMsg = `Specify file type for ${msg} selection`; 15 | const [cancel, type] = await prompt(allMsg, SelectType); 16 | 17 | if (cancel) 18 | return; 19 | 20 | SelectType = type; 21 | 22 | const regExp = getRegExp(type); 23 | let matches = 0; 24 | 25 | for (const current of files) { 26 | const name = DOM.getCurrentName(current); 27 | 28 | if (name === '..' || !regExp.test(name)) 29 | continue; 30 | 31 | ++matches; 32 | 33 | let isSelected = DOM.isSelected(current); 34 | const shouldSel = msg === 'expand'; 35 | 36 | if (shouldSel) 37 | isSelected = !isSelected; 38 | 39 | if (isSelected) 40 | DOM.toggleSelectedFile(current); 41 | } 42 | 43 | if (!matches) 44 | alert('No matches found!'); 45 | }; 46 | -------------------------------------------------------------------------------- /client/dom/storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {parse, stringify} = JSON; 4 | 5 | module.exports.set = async (name, data) => { 6 | localStorage.setItem(name, data); 7 | }; 8 | 9 | module.exports.setJson = async (name, data) => { 10 | localStorage.setItem(name, stringify(data)); 11 | }; 12 | 13 | module.exports.get = async (name) => { 14 | return localStorage.getItem(name); 15 | }; 16 | 17 | module.exports.getJson = async (name) => { 18 | const data = localStorage.getItem(name); 19 | return parse(data); 20 | }; 21 | 22 | module.exports.clear = () => { 23 | localStorage.clear(); 24 | }; 25 | 26 | module.exports.remove = (item) => { 27 | localStorage.removeItem(item); 28 | }; 29 | -------------------------------------------------------------------------------- /client/dom/storage.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const storage = require('./storage'); 6 | 7 | const {stringify} = JSON; 8 | 9 | test('cloudcmd: client: storage: set', async (t) => { 10 | const {localStorage} = global; 11 | const setItem = stub(); 12 | 13 | global.localStorage = { 14 | setItem, 15 | }; 16 | 17 | await storage.set('hello', 'world'); 18 | global.localStorage = localStorage; 19 | 20 | t.calledWith(setItem, ['hello', 'world'], 'should call setItem'); 21 | t.end(); 22 | }); 23 | 24 | test('cloudcmd: client: storage: get', async (t) => { 25 | const {localStorage} = global; 26 | const getItem = stub().returns('world'); 27 | 28 | global.localStorage = { 29 | getItem, 30 | }; 31 | 32 | const result = await storage.get('hello'); 33 | 34 | global.localStorage = localStorage; 35 | 36 | t.equal(result, 'world'); 37 | t.end(); 38 | }); 39 | 40 | test('cloudcmd: client: storage: getJson', async (t) => { 41 | const {localStorage} = global; 42 | const expected = { 43 | hello: 'world', 44 | }; 45 | 46 | const getItem = stub().returns(stringify(expected)); 47 | 48 | global.localStorage = { 49 | getItem, 50 | }; 51 | 52 | const result = await storage.getJson('hello'); 53 | 54 | global.localStorage = localStorage; 55 | 56 | t.deepEqual(result, expected); 57 | t.end(); 58 | }); 59 | 60 | test('cloudcmd: client: storage: setJson', async (t) => { 61 | const {localStorage} = global; 62 | const data = { 63 | hello: 'world', 64 | }; 65 | 66 | const expected = stringify(data); 67 | const setItem = stub(); 68 | 69 | global.localStorage = { 70 | setItem, 71 | }; 72 | 73 | await storage.setJson('hello', data); 74 | global.localStorage = localStorage; 75 | 76 | t.calledWith(setItem, ['hello', expected]); 77 | t.end(); 78 | }); 79 | 80 | test('cloudcmd: client: storage: remove', async (t) => { 81 | const {localStorage} = global; 82 | const removeItem = stub(); 83 | 84 | global.localStorage = { 85 | removeItem, 86 | }; 87 | 88 | await storage.remove('hello'); 89 | global.localStorage = localStorage; 90 | 91 | t.calledWith(removeItem, ['hello'], 'should call removeItem'); 92 | t.end(); 93 | }); 94 | 95 | test('cloudcmd: client: storage: clear', async (t) => { 96 | const {localStorage} = global; 97 | const clear = stub(); 98 | 99 | global.localStorage = { 100 | clear, 101 | }; 102 | 103 | await storage.clear(); 104 | global.localStorage = localStorage; 105 | 106 | t.calledWithNoArgs(clear, 'should call clear'); 107 | t.end(); 108 | }); 109 | -------------------------------------------------------------------------------- /client/dom/upload-files.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | const {eachSeries} = require('execon'); 5 | const wraptile = require('wraptile'); 6 | 7 | const load = require('./load'); 8 | const Images = require('./images'); 9 | const {alert} = require('./dialog'); 10 | 11 | const {FS} = require('../../common/cloudfunc'); 12 | 13 | const {getCurrentDirPath: getPathWhenRootEmpty} = require('.'); 14 | const loadFile = wraptile(_loadFile); 15 | 16 | const onEnd = wraptile(_onEnd); 17 | 18 | module.exports = (dir, files) => { 19 | if (!files) { 20 | files = dir; 21 | dir = getPathWhenRootEmpty(); 22 | } 23 | 24 | const n = files.length; 25 | 26 | if (!n) 27 | return; 28 | 29 | const array = Array.from(files); 30 | const {name} = files[0]; 31 | 32 | eachSeries(array, loadFile(dir, n), onEnd(name)); 33 | }; 34 | 35 | function _onEnd(currentName) { 36 | CloudCmd.refresh({ 37 | currentName, 38 | }); 39 | } 40 | 41 | function _loadFile(dir, n, file, callback) { 42 | let i = 0; 43 | 44 | const {name} = file; 45 | const path = dir + name; 46 | const {prefixURL} = CloudCmd; 47 | const api = prefixURL + FS; 48 | 49 | const percent = (i, n, per = 100) => { 50 | return Math.round(i * per / n); 51 | }; 52 | 53 | const step = (n) => 100 / n; 54 | 55 | ++i; 56 | 57 | load 58 | .put(api + path, file) 59 | .on('error', showError) 60 | .on('end', callback) 61 | .on('progress', (count) => { 62 | const max = step(n); 63 | const value = (i - 1) * max + percent(count, 100, max); 64 | 65 | Images.show.load('top'); 66 | Images.setProgress(Math.round(value)); 67 | }); 68 | } 69 | 70 | function showError({message}) { 71 | alert(message); 72 | } 73 | -------------------------------------------------------------------------------- /client/get-json-from-file-table.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global DOM */ 4 | const Info = DOM.CurrentInfo; 5 | 6 | /** 7 | * Функция генерирует JSON из html-таблицы файлов и 8 | * используеться при первом заходе в корень 9 | */ 10 | module.exports = () => { 11 | const path = DOM.getCurrentDirPath(); 12 | const infoFiles = Info.files || []; 13 | 14 | const notParent = (current) => { 15 | const name = DOM.getCurrentName(current); 16 | return name !== '..'; 17 | }; 18 | 19 | const parse = (current) => { 20 | const name = DOM.getCurrentName(current); 21 | const size = DOM.getCurrentSize(current); 22 | const owner = DOM.getCurrentOwner(current); 23 | const mode = DOM.getCurrentMode(current); 24 | const date = DOM.getCurrentDate(current); 25 | const type = DOM.getCurrentType(current); 26 | 27 | return { 28 | name, 29 | size, 30 | mode, 31 | owner, 32 | date, 33 | type, 34 | }; 35 | }; 36 | 37 | const files = infoFiles 38 | .filter(notParent) 39 | .map(parse); 40 | 41 | const fileTable = { 42 | path, 43 | files, 44 | }; 45 | 46 | return fileTable; 47 | }; 48 | -------------------------------------------------------------------------------- /client/key/binder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.createBinder = () => { 4 | let binded = false; 5 | 6 | return { 7 | isBind() { 8 | return binded; 9 | }, 10 | setBind() { 11 | binded = true; 12 | }, 13 | unsetBind() { 14 | binded = false; 15 | }, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /client/key/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('css-modules-require-hook/preset'); 4 | 5 | const autoGlobals = require('auto-globals'); 6 | const mockRequire = require('mock-require'); 7 | const supertape = require('supertape'); 8 | 9 | const {ESC} = require('./key'); 10 | const {getDOM, getCloudCmd} = require('./vim/globals.fixture'); 11 | const test = autoGlobals(supertape); 12 | const {reRequire, stopAll} = mockRequire; 13 | const {stub} = supertape; 14 | 15 | global.DOM = getDOM(); 16 | global.CloudCmd = getCloudCmd(); 17 | 18 | test('cloudcmd: client: key: enable vim', async (t) => { 19 | const vim = stub(); 20 | const {CloudCmd} = global; 21 | const {config} = CloudCmd; 22 | 23 | CloudCmd.config = stub().returns(true); 24 | CloudCmd._config = stub(); 25 | mockRequire('./vim', vim); 26 | const {_listener, setBind} = reRequire('.'); 27 | 28 | const event = { 29 | keyCode: ESC, 30 | key: 'Escape', 31 | altKey: false, 32 | }; 33 | 34 | setBind(); 35 | await _listener(event); 36 | 37 | CloudCmd.config = config; 38 | stopAll(); 39 | 40 | t.calledWith(vim, ['Escape', event]); 41 | t.end(); 42 | }); 43 | 44 | test('cloudcmd: client: key: disable vim', async (t) => { 45 | const _config = stub(); 46 | const event = { 47 | keyCode: ESC, 48 | key: 'Escape', 49 | altKey: false, 50 | }; 51 | 52 | const {CloudCmd} = global; 53 | const {config} = CloudCmd; 54 | 55 | global.CloudCmd.config = _config; 56 | global.CloudCmd._config = _config; 57 | 58 | const {_listener, setBind} = reRequire('.'); 59 | 60 | setBind(); 61 | await _listener(event); 62 | 63 | CloudCmd.config = config; 64 | 65 | t.calledWith(_config, ['vim']); 66 | t.end(); 67 | }); 68 | -------------------------------------------------------------------------------- /client/key/key.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | BACKSPACE: 8, 5 | TAB: 9, 6 | ENTER: 13, 7 | CAPSLOCK: 20, 8 | ESC: 27, 9 | 10 | SPACE: 32, 11 | PAGE_UP: 33, 12 | PAGE_DOWN: 34, 13 | END: 35, 14 | HOME: 36, 15 | 16 | LEFT: 37, 17 | UP: 38, 18 | RIGHT: 39, 19 | DOWN: 40, 20 | 21 | INSERT: 45, 22 | DELETE: 46, 23 | 24 | ZERO: 48, 25 | 26 | SEMICOLON: 52, 27 | 28 | A: 65, 29 | 30 | C: 67, 31 | D: 68, 32 | 33 | G: 71, 34 | 35 | J: 74, 36 | K: 75, 37 | 38 | M: 77, 39 | 40 | O: 79, 41 | P: 80, 42 | Q: 81, 43 | R: 82, 44 | S: 83, 45 | T: 84, 46 | U: 85, 47 | 48 | V: 86, 49 | 50 | X: 88, 51 | 52 | Z: 90, 53 | 54 | INSERT_MAC: 96, 55 | 56 | ASTERISK: 106, 57 | PLUS: 107, 58 | MINUS: 109, 59 | 60 | F1: 112, 61 | F2: 113, 62 | F3: 114, 63 | F4: 115, 64 | F5: 116, 65 | F6: 117, 66 | F7: 118, 67 | F8: 119, 68 | F9: 120, 69 | F10: 121, 70 | 71 | COLON: 186, 72 | EQUAL: 187, 73 | HYPHEN: 189, 74 | DOT: 190, 75 | SLASH: 191, 76 | /* Typewritten Reverse Apostrophe (`) */ 77 | TRA: 192, 78 | BACKSLASH: 220, 79 | 80 | BRACKET_CLOSE: 221, 81 | }; 82 | -------------------------------------------------------------------------------- /client/key/set-current-by-char.js: -------------------------------------------------------------------------------- 1 | /* global DOM */ 2 | 3 | 'use strict'; 4 | 5 | const {escapeRegExp} = require('../../common/util'); 6 | const Info = DOM.CurrentInfo; 7 | 8 | module.exports = function setCurrentByChar(char, charStore) { 9 | let firstByName; 10 | let skipCount = 0; 11 | let setted = false; 12 | let i = 0; 13 | 14 | const escapeChar = escapeRegExp(char); 15 | const regExp = new RegExp(`^${escapeChar}.*$`, 'i'); 16 | const {files} = Info; 17 | const chars = charStore(); 18 | const n = chars.length; 19 | 20 | while (i < n && char === chars[i]) 21 | i++; 22 | 23 | if (!i) 24 | charStore([]); 25 | 26 | const skipN = skipCount = i; 27 | 28 | charStore(charStore().concat(char)); 29 | 30 | const names = DOM.getFilenames(files); 31 | const isTest = (a) => regExp.test(a); 32 | const isRoot = (a) => a === '..'; 33 | const not = (f) => (a) => !f(a); 34 | 35 | const setCurrent = (name) => { 36 | const byName = DOM.getCurrentByName(name); 37 | 38 | if (!skipCount) { 39 | setted = true; 40 | DOM.setCurrentFile(byName); 41 | 42 | return true; 43 | } 44 | 45 | if (skipN === skipCount) 46 | firstByName = byName; 47 | 48 | --skipCount; 49 | }; 50 | 51 | names 52 | .filter(isTest) 53 | .filter(not(isRoot)) 54 | .some(setCurrent); 55 | 56 | if (!setted) { 57 | DOM.setCurrentFile(firstByName); 58 | charStore([char]); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /client/key/vim/find.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fullstore = require('fullstore'); 4 | const limier = require('limier'); 5 | 6 | const searchStore = fullstore([]); 7 | const searchIndex = fullstore(0); 8 | 9 | module.exports.find = (value, names) => { 10 | const result = limier(value, names); 11 | 12 | searchStore(result); 13 | searchIndex(0); 14 | 15 | return result; 16 | }; 17 | 18 | module.exports.findNext = () => { 19 | const names = searchStore(); 20 | const index = next(searchIndex(), names.length); 21 | 22 | searchIndex(index); 23 | return names[searchIndex()]; 24 | }; 25 | 26 | module.exports.findPrevious = () => { 27 | const names = searchStore(); 28 | const index = previous(searchIndex(), names.length); 29 | 30 | searchIndex(index); 31 | return names[index]; 32 | }; 33 | 34 | module.exports._next = next; 35 | module.exports._previous = previous; 36 | 37 | function next(index, length) { 38 | if (index === length - 1) 39 | return 0; 40 | 41 | return ++index; 42 | } 43 | 44 | function previous(index, length) { 45 | if (!index) 46 | return length - 1; 47 | 48 | return --index; 49 | } 50 | -------------------------------------------------------------------------------- /client/key/vim/find.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const dir = './'; 5 | 6 | const {getDOM} = require('./globals.fixture'); 7 | 8 | global.DOM = getDOM(); 9 | 10 | const {_next, _previous} = require(`${dir}find`); 11 | 12 | test('cloudcmd: client: vim: _next', (t) => { 13 | const result = _next(1, 2); 14 | 15 | t.notOk(result, 'should return 0'); 16 | t.end(); 17 | }); 18 | 19 | test('cloudcmd: client: vim: _previous', (t) => { 20 | const result = _previous(0, 2); 21 | 22 | t.equal(result, 1, 'should return 1'); 23 | t.end(); 24 | }); 25 | -------------------------------------------------------------------------------- /client/key/vim/globals.fixture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const noop = () => {}; 4 | 5 | module.exports.getDOM = () => { 6 | const prompt = Promise.resolve.bind(Promise); 7 | const CurrentInfo = { 8 | element: {}, 9 | files: [], 10 | }; 11 | 12 | const Buffer = { 13 | copy: noop, 14 | paste: noop, 15 | }; 16 | 17 | const Dialog = { 18 | prompt, 19 | }; 20 | 21 | return { 22 | Buffer, 23 | CurrentInfo, 24 | Dialog, 25 | selectFile: noop, 26 | unselectFile: noop, 27 | unselectFiles: noop, 28 | setCurrentFile: noop, 29 | getCurrentName: noop, 30 | setCurrentByName: noop, 31 | toggleSelectedFile: noop, 32 | prompNewDirectory: noop, 33 | promptNewFile: noop, 34 | }; 35 | }; 36 | 37 | module.exports.getCloudCmd = () => { 38 | const show = () => {}; 39 | 40 | return { 41 | Operation: { 42 | show, 43 | }, 44 | 45 | config: noop, 46 | _config: noop, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /client/key/vim/set-current.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global DOM */ 4 | module.exports.selectFileNotParent = selectFileNotParent; 5 | function selectFileNotParent(current, {getCurrentName, selectFile} = DOM) { 6 | const name = getCurrentName(current); 7 | 8 | if (name === '..') 9 | return; 10 | 11 | selectFile(current); 12 | } 13 | 14 | module.exports.setCurrent = (sibling, {count, isVisual, isDelete}, {Info, setCurrentFile, unselectFiles, Operation}) => { 15 | let current = Info.element; 16 | const select = isVisual ? selectFileNotParent : unselectFiles; 17 | 18 | select(current); 19 | 20 | const position = `${sibling}Sibling`; 21 | 22 | for (let i = 0; i < count; i++) { 23 | const next = current[position]; 24 | 25 | if (!next) 26 | break; 27 | 28 | current = next; 29 | select(current); 30 | } 31 | 32 | setCurrentFile(current); 33 | 34 | if (isDelete) 35 | Operation.show('delete'); 36 | }; 37 | -------------------------------------------------------------------------------- /client/key/vim/vim.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const vim = require('./vim'); 6 | 7 | test('vim: no operations', (t) => { 8 | const result = vim('hello', {}); 9 | 10 | t.notOk(result); 11 | t.end(); 12 | }); 13 | 14 | test('vim: space', (t) => { 15 | const moveNext = stub(); 16 | 17 | vim(' '); 18 | vim('j', { 19 | moveNext, 20 | }); 21 | 22 | const args = [{ 23 | count: 1, 24 | isDelete: false, 25 | isVisual: false, 26 | }]; 27 | 28 | t.calledWith(moveNext, args); 29 | t.end(); 30 | }); 31 | 32 | test('vim: ^', (t) => { 33 | const movePrevious = stub(); 34 | 35 | vim('^', { 36 | movePrevious, 37 | }); 38 | 39 | const expected = { 40 | count: Infinity, 41 | isVisual: false, 42 | isDelete: false, 43 | }; 44 | 45 | t.calledWith(movePrevious, [expected], 'should call movePrevious'); 46 | t.end(); 47 | }); 48 | 49 | test('vim: w', (t) => { 50 | const moveNext = stub(); 51 | 52 | vim('w', { 53 | moveNext, 54 | }); 55 | 56 | const expected = { 57 | count: 1, 58 | isVisual: false, 59 | isDelete: false, 60 | }; 61 | 62 | t.calledWith(moveNext, [expected], 'should call moveNext'); 63 | t.end(); 64 | }); 65 | 66 | test('vim: b', (t) => { 67 | const movePrevious = stub(); 68 | 69 | vim('b', { 70 | movePrevious, 71 | }); 72 | 73 | const expected = { 74 | count: 1, 75 | isVisual: false, 76 | isDelete: false, 77 | }; 78 | 79 | t.calledWith(movePrevious, [expected], 'should call movePrevious'); 80 | t.end(); 81 | }); 82 | -------------------------------------------------------------------------------- /client/listeners/get-index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (array, item) => { 4 | const index = array.indexOf(item); 5 | 6 | if (!~index) 7 | return 0; 8 | 9 | return index; 10 | }; 11 | -------------------------------------------------------------------------------- /client/listeners/get-range.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (indexFrom, indexTo, files) => { 4 | if (indexFrom < indexTo) 5 | return files.slice(indexFrom, indexTo + 1); 6 | 7 | if (indexFrom > indexTo) 8 | return files.slice(indexTo, indexFrom + 1); 9 | 10 | return [files[indexFrom]]; 11 | }; 12 | -------------------------------------------------------------------------------- /client/load-module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | const exec = require('execon'); 5 | const tryToCatch = require('try-to-catch'); 6 | const loadJS = require('load.js').js; 7 | 8 | const pascalCase = require('just-pascal-case'); 9 | const noJS = (a) => a.replace(/.js$/, ''); 10 | 11 | /** 12 | * function load modules 13 | * @params = {name, path, func, dobefore, arg} 14 | */ 15 | module.exports = function loadModule(params) { 16 | if (!params) 17 | return; 18 | 19 | const {path} = params; 20 | 21 | const name = path && noJS(pascalCase(path)); 22 | const doBefore = params.dobefore; 23 | 24 | if (CloudCmd[name]) 25 | return; 26 | 27 | CloudCmd[name] = async () => { 28 | exec(doBefore); 29 | 30 | const {DIR_MODULES} = CloudCmd; 31 | const pathFull = `${DIR_MODULES}/${path}.js`; 32 | 33 | await loadJS(pathFull); 34 | const newModule = async (f) => f && f(); 35 | const module = CloudCmd[name]; 36 | 37 | Object.assign(newModule, module); 38 | 39 | CloudCmd[name] = newModule; 40 | CloudCmd.log('init', name); 41 | 42 | await module.init(); 43 | 44 | return newModule; 45 | }; 46 | 47 | CloudCmd[name].show = async (...args) => { 48 | CloudCmd.log('show', name, args); 49 | const m = CloudCmd[name]; 50 | 51 | const [e, a] = await tryToCatch(m); 52 | 53 | if (e) 54 | return console.error(e); 55 | 56 | return await a.show(...args); 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /client/modules/cloud.js: -------------------------------------------------------------------------------- 1 | /* global CloudCmd, filepicker */ 2 | 3 | 'use strict'; 4 | 5 | const exec = require('execon'); 6 | const currify = require('currify'); 7 | const load = require('load.js'); 8 | 9 | const {ajax} = require('../dom/load'); 10 | 11 | const Files = require('../dom/files'); 12 | const Images = require('../dom/images'); 13 | const {log} = CloudCmd; 14 | 15 | const upload = currify(_upload); 16 | 17 | const Name = 'Cloud'; 18 | 19 | CloudCmd[Name] = module.exports; 20 | 21 | module.exports.init = async () => { 22 | const [modules] = await loadFiles(); 23 | const {key} = modules.data.FilePicker; 24 | 25 | filepicker.setKey(key); 26 | Images.hide(); 27 | }; 28 | 29 | module.exports.uploadFile = (filename, data) => { 30 | const mimetype = ''; 31 | 32 | filepicker.store(data, { 33 | mimetype, 34 | filename, 35 | }, (fpFile) => { 36 | filepicker.exportFile(fpFile, log, log); 37 | }); 38 | }; 39 | 40 | module.exports.saveFile = (callback) => { 41 | filepicker.pick(upload(callback)); 42 | }; 43 | 44 | function _upload(callback, file) { 45 | const {url, filename} = file; 46 | 47 | const responseType = 'arraybuffer'; 48 | const success = exec.with(callback, filename); 49 | 50 | ajax({ 51 | url, 52 | responseType, 53 | success, 54 | }); 55 | } 56 | 57 | function loadFiles() { 58 | const js = '//api.filepicker.io/v2/filepicker.js'; 59 | 60 | return Promise.all([ 61 | Files.get('modules'), 62 | load.js(js), 63 | ]); 64 | } 65 | -------------------------------------------------------------------------------- /client/modules/command-line.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | CloudCmd.CommandLine = exports; 5 | 6 | const Dialog = require('../dom/dialog'); 7 | 8 | const noop = () => {}; 9 | 10 | module.exports.init = noop; 11 | 12 | module.exports.show = show; 13 | module.exports.hide = hide; 14 | 15 | async function show() { 16 | const [, cmd] = await Dialog.prompt('Command Line', ''); 17 | const TERMINAL = '^(t|terminal)'; 18 | 19 | if (RegExp(`${TERMINAL}$`).test(cmd)) 20 | return await CloudCmd.Terminal.show(); 21 | 22 | if (RegExp(TERMINAL).test(cmd)) { 23 | const command = cmd.replace(RegExp(`${TERMINAL} `), ''); 24 | const exitCode = await CloudCmd.TerminalRun.show({ 25 | command: `bash -c '${command}'`, 26 | }); 27 | 28 | if (exitCode === -1) 29 | await Dialog.alert(`☝️ Looks like Terminal is disabled, start Cloud Coammnder with '--terminal' flag.`); 30 | 31 | return; 32 | } 33 | } 34 | 35 | function hide() {} 36 | -------------------------------------------------------------------------------- /client/modules/config/input.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const currify = require('currify'); 4 | 5 | const isType = currify((type, object, name) => type === typeof object[name]); 6 | 7 | const isBool = isType('boolean'); 8 | 9 | module.exports.getElementByName = getElementByName; 10 | 11 | function getElementByName(selector, element) { 12 | const str = `[data-name="js-${selector}"]`; 13 | 14 | return element.querySelector(str); 15 | } 16 | 17 | module.exports.getName = (element) => { 18 | const name = element 19 | .getAttribute('data-name') 20 | .replace(/^js-/, ''); 21 | 22 | return name; 23 | }; 24 | 25 | module.exports.convert = (config) => { 26 | const result = config; 27 | const array = Object.keys(config); 28 | 29 | const filtered = array.filter(isBool(config)); 30 | 31 | for (const name of filtered) { 32 | const item = config[name]; 33 | result[name] = setState(item); 34 | } 35 | 36 | return result; 37 | }; 38 | 39 | function setState(state) { 40 | if (state) 41 | return ' checked'; 42 | 43 | return ''; 44 | } 45 | 46 | module.exports.getValue = (name, element) => { 47 | const el = getElementByName(name, element); 48 | const {type} = el; 49 | 50 | switch(type) { 51 | case 'checkbox': 52 | return el.checked; 53 | 54 | case 'number': 55 | return Number(el.value); 56 | 57 | default: 58 | return el.value; 59 | } 60 | }; 61 | 62 | module.exports.setValue = (name, value, element) => { 63 | const el = getElementByName(name, element); 64 | const {type} = el; 65 | 66 | switch(type) { 67 | case 'checkbox': 68 | el.checked = value; 69 | break; 70 | 71 | default: 72 | el.value = value; 73 | break; 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /client/modules/contact.js: -------------------------------------------------------------------------------- 1 | /* global CloudCmd */ 2 | /* global DOM */ 3 | 4 | 'use strict'; 5 | 6 | CloudCmd.Contact = exports; 7 | 8 | const olark = require('@cloudcmd/olark'); 9 | const Images = require('../dom/images'); 10 | 11 | const {Events} = DOM; 12 | const {Key} = CloudCmd; 13 | 14 | module.exports.show = show; 15 | module.exports.hide = hide; 16 | 17 | module.exports.init = () => { 18 | Events.addKey(onKey); 19 | 20 | olark.identify('6216-545-10-4223'); 21 | olark('api.box.onExpand', show); 22 | olark('api.box.onShow', show); 23 | olark('api.box.onShrink', hide); 24 | }; 25 | 26 | function show() { 27 | Key.unsetBind(); 28 | Images.hide(); 29 | 30 | olark('api.box.expand'); 31 | } 32 | 33 | function hide() { 34 | Key.setBind(); 35 | olark('api.box.hide'); 36 | } 37 | 38 | function onKey({keyCode}) { 39 | if (keyCode === Key.ESC) 40 | hide(); 41 | } 42 | -------------------------------------------------------------------------------- /client/modules/edit-file-vim.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | CloudCmd.EditFileVim = exports; 5 | 6 | const Events = require('../dom/events'); 7 | 8 | const {Key} = CloudCmd; 9 | 10 | const ConfigView = { 11 | bindKeys: false, 12 | beforeClose: () => { 13 | Events.rmKey(listener); 14 | CloudCmd.EditFile.isChanged(); 15 | }, 16 | }; 17 | 18 | module.exports.init = async () => { 19 | await CloudCmd.EditFile(); 20 | }; 21 | 22 | module.exports.show = async () => { 23 | Events.addKey(listener); 24 | 25 | const editFile = await CloudCmd.EditFile.show(ConfigView); 26 | 27 | editFile 28 | .getEditor() 29 | .setKeyMap('vim'); 30 | }; 31 | 32 | module.exports.hide = hide; 33 | 34 | function hide() { 35 | CloudCmd.Edit.hide(); 36 | } 37 | 38 | function listener(event) { 39 | const {keyCode, shiftKey} = event; 40 | 41 | if (shiftKey && keyCode === Key.ESC) { 42 | event.preventDefault(); 43 | hide(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/modules/edit-names-vim.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | CloudCmd.EditNamesVim = exports; 5 | 6 | const Events = require('../dom/events'); 7 | const {Key} = CloudCmd; 8 | 9 | const ConfigView = { 10 | bindKeys: false, 11 | beforeClose: () => { 12 | Events.rmKey(listener); 13 | CloudCmd.EditNames.isChanged(); 14 | }, 15 | }; 16 | 17 | module.exports.init = async () => { 18 | await CloudCmd.EditNames(); 19 | }; 20 | 21 | module.exports.show = () => { 22 | Events.addKey(listener); 23 | 24 | CloudCmd 25 | .EditNames 26 | .show(ConfigView) 27 | .getEditor() 28 | .setKeyMap('vim'); 29 | }; 30 | 31 | module.exports.hide = hide; 32 | 33 | function hide() { 34 | CloudCmd.Edit.hide(); 35 | } 36 | 37 | function listener(event) { 38 | const {keyCode, shiftKey} = event; 39 | 40 | if (shiftKey && keyCode === Key.ESC) { 41 | event.preventDefault(); 42 | hide(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/modules/help.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | CloudCmd.Help = exports; 5 | 6 | const Images = require('../dom/images'); 7 | 8 | module.exports.init = () => { 9 | Images.show.load('top'); 10 | }; 11 | 12 | module.exports.show = show; 13 | module.exports.hide = hide; 14 | 15 | function show() { 16 | const positionLoad = 'top'; 17 | const relative = true; 18 | 19 | CloudCmd.Markdown.show('/HELP.md', { 20 | positionLoad, 21 | relative, 22 | }); 23 | } 24 | 25 | function hide() { 26 | CloudCmd.View.hide(); 27 | } 28 | -------------------------------------------------------------------------------- /client/modules/markdown.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | CloudCmd.Markdown = exports; 5 | 6 | const createElement = require('@cloudcmd/create-element'); 7 | 8 | const Images = require('../dom/images'); 9 | const {Markdown} = require('../dom/rest'); 10 | const {alert} = require('../dom/dialog'); 11 | 12 | module.exports.init = async () => { 13 | Images.show.load('top'); 14 | await CloudCmd.View(); 15 | }; 16 | 17 | module.exports.show = show; 18 | 19 | module.exports.hide = () => { 20 | CloudCmd.View.hide(); 21 | }; 22 | 23 | async function show(name, options = {}) { 24 | const {positionLoad, relative} = options; 25 | 26 | Images.show.load(positionLoad); 27 | 28 | if (relative) 29 | name += '?relative'; 30 | 31 | const [error, innerHTML] = await Markdown.read(name); 32 | Images.hide(); 33 | 34 | if (error) 35 | return alert(error.message, { 36 | cancel: false, 37 | }); 38 | 39 | const className = 'help'; 40 | 41 | const div = createElement('div', { 42 | className, 43 | innerHTML, 44 | }); 45 | 46 | CloudCmd.View.show(div); 47 | } 48 | -------------------------------------------------------------------------------- /client/modules/operation/format.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (operation, from, to) => { 4 | if (!to) 5 | return `${operation} ${from}`; 6 | 7 | return `${operation} ${from} -> ${to}`; 8 | }; 9 | -------------------------------------------------------------------------------- /client/modules/operation/get-next-current-name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const currify = require('currify'); 4 | 5 | const not = currify((array, value) => !array.includes(value)); 6 | const notOneOf = currify((a, b) => a.filter(not(b))); 7 | 8 | module.exports = (currentName, names, removedNames) => { 9 | const i = names.indexOf(currentName); 10 | 11 | const nextNames = notOneOf(names, removedNames); 12 | const {length} = nextNames; 13 | 14 | if (nextNames[i]) 15 | return nextNames[i]; 16 | 17 | return nextNames[length - 1]; 18 | }; 19 | -------------------------------------------------------------------------------- /client/modules/operation/remove-extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {getExt} = require('../../../common/util'); 4 | 5 | module.exports = (name) => { 6 | const ext = getExtension(name); 7 | 8 | return name.replace(ext, ''); 9 | }; 10 | 11 | function getExtension(name) { 12 | if (/\.tar\.gz$/.test(name)) 13 | return '.tar.gz'; 14 | 15 | if (/\.tar\.bz2$/.test(name)) 16 | return '.tar.bz2'; 17 | 18 | return getExt(name); 19 | } 20 | -------------------------------------------------------------------------------- /client/modules/operation/remove-extension.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const removeExtension = require(`./remove-extension`); 5 | 6 | test('cloudcmd: client: modules: operation: removeExtension: .tar.gz', (t) => { 7 | const name = 'hello'; 8 | const fullName = `${name}.tar.gz`; 9 | 10 | t.equal(removeExtension(fullName), name, 'should remove .tar.gz'); 11 | t.end(); 12 | }); 13 | 14 | test('cloudcmd: client: modules: operation: removeExtension: .tar.bz2', (t) => { 15 | const name = 'hello'; 16 | const fullName = `${name}.tar.bz2`; 17 | 18 | t.equal(removeExtension(fullName), name, 'should remove .tar.bz2'); 19 | t.end(); 20 | }); 21 | 22 | test('cloudcmd: client: modules: operation: removeExtension: .bz2', (t) => { 23 | const name = 'hello'; 24 | const fullName = `${name}.bz2`; 25 | 26 | t.equal(removeExtension(fullName), name, 'should remove .bz2'); 27 | t.end(); 28 | }); 29 | -------------------------------------------------------------------------------- /client/modules/operation/set-listeners.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global DOM */ 4 | const forEachKey = require('for-each-key'); 5 | 6 | const wraptile = require('wraptile'); 7 | const format = require('./format'); 8 | 9 | const {Dialog, Images} = DOM; 10 | 11 | module.exports = (options) => (emitter) => { 12 | const { 13 | operation, 14 | callback, 15 | noContinue, 16 | from, 17 | to, 18 | } = options; 19 | 20 | let done; 21 | let lastError; 22 | 23 | const onAbort = wraptile(({emitter, operation}) => { 24 | emitter.abort(); 25 | 26 | const msg = `${operation} aborted`; 27 | 28 | lastError = true; 29 | 30 | Dialog.alert(msg, { 31 | cancel: false, 32 | }); 33 | }); 34 | 35 | const removeListener = emitter.removeListener.bind(emitter); 36 | const on = emitter.on.bind(emitter); 37 | 38 | const message = format(operation, from, to); 39 | const progress = Dialog.progress(message); 40 | 41 | progress.catch(onAbort({ 42 | emitter, 43 | operation, 44 | })); 45 | 46 | const listeners = { 47 | progress: (value) => { 48 | done = value === 100; 49 | progress.setProgress(value); 50 | }, 51 | 52 | end: () => { 53 | Images.hide(); 54 | forEachKey(removeListener, listeners); 55 | progress.remove(); 56 | 57 | if (lastError || done) 58 | callback(); 59 | }, 60 | 61 | error: async (error) => { 62 | lastError = error; 63 | 64 | if (noContinue) { 65 | listeners.end(error); 66 | Dialog.alert(error); 67 | progress.remove(); 68 | 69 | return; 70 | } 71 | 72 | const [cancel] = await Dialog.confirm(`${error} 73 | Continue?`); 74 | 75 | if (!done && !cancel) 76 | return emitter.continue(); 77 | 78 | emitter.abort(); 79 | progress.remove(); 80 | }, 81 | }; 82 | 83 | forEachKey(on, listeners); 84 | }; 85 | -------------------------------------------------------------------------------- /client/modules/polyfill.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global DOM */ 4 | require('domtokenlist-shim'); 5 | 6 | const scrollIntoViewIfNeeded = require('scroll-into-view-if-needed').default; 7 | 8 | DOM.scrollIntoViewIfNeeded = (el) => scrollIntoViewIfNeeded(el, { 9 | block: 'nearest', 10 | }); 11 | -------------------------------------------------------------------------------- /client/modules/polyfill.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const mockRequire = require('mock-require'); 6 | 7 | const {stopAll} = mockRequire; 8 | 9 | test('cloudcmd: client: polyfill: scrollIntoViewIfNeaded', (t) => { 10 | const {DOM} = global; 11 | const scroll = stub(); 12 | const el = {}; 13 | 14 | global.DOM = {}; 15 | 16 | mockRequire('scroll-into-view-if-needed', { 17 | default: scroll, 18 | }); 19 | 20 | mockRequire.reRequire('./polyfill'); 21 | 22 | global.DOM.scrollIntoViewIfNeeded(el); 23 | mockRequire.stop('scroll-into-view-if-neaded'); 24 | global.DOM = DOM; 25 | 26 | const args = [ 27 | el, { 28 | block: 'nearest', 29 | }, 30 | ]; 31 | 32 | stopAll(); 33 | 34 | t.calledWith(scroll, args, 'should call scrollIntoViewIfNeaded'); 35 | t.end(); 36 | }); 37 | -------------------------------------------------------------------------------- /client/modules/terminal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | /* global gritty */ 5 | const tryToCatch = require('try-to-catch'); 6 | 7 | require('../../css/terminal.css'); 8 | 9 | const exec = require('execon'); 10 | const load = require('load.js'); 11 | const DOM = require('../dom'); 12 | const Images = require('../dom/images'); 13 | 14 | const loadParallel = load.parallel; 15 | 16 | const {Dialog} = DOM; 17 | const {Key, config} = CloudCmd; 18 | 19 | CloudCmd.Terminal = exports; 20 | 21 | let Loaded; 22 | let Terminal; 23 | let Socket; 24 | 25 | const loadAll = async () => { 26 | const {prefix} = CloudCmd; 27 | 28 | const prefixGritty = getPrefix(); 29 | const js = `${prefixGritty}/gritty.js`; 30 | const css = `${prefix}/dist/terminal.css`; 31 | 32 | const [e] = await tryToCatch(loadParallel, [js, css]); 33 | 34 | if (e) { 35 | const src = e.target.src.replace(window.location.href, ''); 36 | return Dialog.alert(`file ${src} could not be loaded`); 37 | } 38 | 39 | Loaded = true; 40 | }; 41 | 42 | module.exports.init = async () => { 43 | if (!config('terminal')) 44 | return; 45 | 46 | Images.show.load('top'); 47 | 48 | await CloudCmd.View(); 49 | await loadAll(); 50 | create(); 51 | }; 52 | 53 | module.exports.show = show; 54 | module.exports.hide = hide; 55 | 56 | function hide() { 57 | CloudCmd.View.hide(); 58 | } 59 | 60 | const getPrefix = () => CloudCmd.prefix + '/gritty'; 61 | 62 | function getPrefixSocket() { 63 | return CloudCmd.prefixSocket + '/gritty'; 64 | } 65 | 66 | const getEnv = () => ({ 67 | ACTIVE_DIR: DOM.getCurrentDirPath, 68 | PASSIVE_DIR: DOM.getNotCurrentDirPath, 69 | CURRENT_NAME: DOM.getCurrentName, 70 | CURRENT_PATH: DOM.getCurrentPath, 71 | }); 72 | 73 | function create() { 74 | const options = { 75 | env: getEnv(), 76 | prefix: getPrefixSocket(), 77 | socketPath: CloudCmd.prefix, 78 | fontFamily: 'Droid Sans Mono', 79 | }; 80 | 81 | const {socket, terminal} = gritty(document.body, options); 82 | 83 | Socket = socket; 84 | Terminal = terminal; 85 | 86 | Terminal.onKey(({domEvent}) => { 87 | const {keyCode, shiftKey} = domEvent; 88 | 89 | if (shiftKey && keyCode === Key.ESC) 90 | hide(); 91 | }); 92 | 93 | Socket.on('connect', exec.with(authCheck, socket)); 94 | } 95 | 96 | function authCheck(spawn) { 97 | spawn.emit('auth', config('username'), config('password')); 98 | 99 | spawn.on('reject', () => { 100 | Dialog.alert('Wrong credentials!'); 101 | }); 102 | } 103 | 104 | function show() { 105 | if (!Loaded) 106 | return; 107 | 108 | if (!config('terminal')) 109 | return; 110 | 111 | CloudCmd.View.show(Terminal.element, { 112 | afterShow: () => { 113 | Terminal.focus(); 114 | }, 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /client/modules/upload.js: -------------------------------------------------------------------------------- 1 | /* global CloudCmd, DOM */ 2 | 3 | 'use strict'; 4 | 5 | CloudCmd.Upload = exports; 6 | 7 | const createElement = require('@cloudcmd/create-element'); 8 | const Files = require('../dom/files'); 9 | const Images = require('../dom/images'); 10 | const uploadFiles = require('../dom/upload-files'); 11 | 12 | module.exports.init = async () => { 13 | Images.show.load('top'); 14 | await CloudCmd.View(); 15 | }; 16 | 17 | module.exports.show = show; 18 | module.exports.hide = hide; 19 | 20 | async function show() { 21 | Images.show.load('top'); 22 | 23 | const innerHTML = await Files.get('upload'); 24 | const autoSize = true; 25 | 26 | const el = createElement('div', { 27 | innerHTML, 28 | }); 29 | 30 | CloudCmd.View.show(el, { 31 | autoSize, 32 | afterShow, 33 | }); 34 | 35 | const fontFamily = [ 36 | '"Droid Sans Mono"', 37 | '"Ubuntu Mono"', 38 | '"Consolas"', 39 | 'monospace', 40 | ].join(', '); 41 | 42 | createElement('style', { 43 | dataName: 'upload-css', 44 | innerText: `[data-name=js-upload-file-button] { 45 | font-family: ${fontFamily}; 46 | font-size: 16px; 47 | margin: 10px 0 10px 0; 48 | }`, 49 | }); 50 | } 51 | 52 | function hide() { 53 | CloudCmd.View.hide(); 54 | } 55 | 56 | function afterShow() { 57 | const button = DOM.getByDataName('js-upload-file-button'); 58 | 59 | Images.hide(); 60 | 61 | DOM.Events.add('change', button, ({target}) => { 62 | const {files} = target; 63 | 64 | hide(); 65 | 66 | uploadFiles(files); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /client/modules/user-menu/get-user-menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (menuFn) => { 4 | const module = {}; 5 | const fn = Function('module', menuFn); 6 | 7 | fn(module); 8 | 9 | return module.exports; 10 | }; 11 | -------------------------------------------------------------------------------- /client/modules/user-menu/get-user-menu.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const getUserMenu = require('./get-user-menu'); 5 | 6 | test('user-menu: getUserMenu', (t) => { 7 | const menu = `module.exports = { 8 | 'F2 - Rename file': ({DOM}) => { 9 | const {element} = DOM.CurrentInfo; 10 | DOM.renameCurrent(element); 11 | } 12 | }`; 13 | 14 | const result = getUserMenu(menu); 15 | 16 | const [key] = Object.keys(result); 17 | 18 | t.equal(key, 'F2 - Rename file'); 19 | t.end(); 20 | }); 21 | -------------------------------------------------------------------------------- /client/modules/user-menu/navigate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fullstore = require('fullstore'); 4 | 5 | const {J, K, UP, DOWN} = require('../../key/key.js'); 6 | 7 | const store = fullstore(1); 8 | const isDigit = (a) => /^\d+$/.test(a); 9 | 10 | module.exports = (el, {key, keyCode}) => { 11 | if (isDigit(key)) 12 | store(Number(key)); 13 | 14 | if (keyCode === DOWN || keyCode === J) { 15 | const count = store(); 16 | store(1); 17 | 18 | return down(el, count); 19 | } 20 | 21 | if (keyCode === UP || keyCode === K) { 22 | const count = store(); 23 | store(1); 24 | 25 | return up(el, count); 26 | } 27 | }; 28 | 29 | function down(el, count) { 30 | const {length} = el; 31 | 32 | if (el.selectedIndex === length - 1) 33 | el.selectedIndex = 0; 34 | else 35 | el.selectedIndex += count; 36 | 37 | if (el.selectedIndex < 0) 38 | el.selectedIndex = length - 1; 39 | } 40 | 41 | function up(el, count) { 42 | const {length} = el; 43 | 44 | if (!el.selectedIndex) 45 | el.selectedIndex = length - 1; 46 | else 47 | el.selectedIndex -= count; 48 | 49 | if (el.selectedIndex < 0) 50 | el.selectedIndex = 0; 51 | } 52 | -------------------------------------------------------------------------------- /client/modules/user-menu/parse-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isNumber = (a) => typeof a === 'number'; 4 | 5 | module.exports = (error) => { 6 | const {lineNumber, columnNumber} = error; 7 | 8 | // thank you firefox 9 | if (isNumber(lineNumber) && isNumber(columnNumber)) 10 | return [lineNumber, columnNumber]; 11 | 12 | const before = error.stack.indexOf('>'); 13 | const str = error.stack.slice(before + 1); 14 | const after = str.indexOf(')'); 15 | const newStr = str.slice(1, after); 16 | 17 | const [line, column] = newStr.split(':'); 18 | 19 | return [ 20 | Number(line), 21 | Number(column), 22 | ]; 23 | }; 24 | -------------------------------------------------------------------------------- /client/modules/user-menu/parse-error.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const parseError = require('./parse-error'); 5 | 6 | test('user-menu: parse-error', (t) => { 7 | const result = parseError({ 8 | lineNumber: 1, 9 | columnNumber: 2, 10 | }); 11 | 12 | const expected = [1, 2]; 13 | 14 | t.deepEqual(result, expected); 15 | t.end(); 16 | }); 17 | 18 | test('user-menu: parse-error: stack', (t) => { 19 | const stack = ` 20 | ReferenceError: s is not defined 21 | at eval (eval at module.exports (get-user-menu.js:NaN), :1:2) 22 | at module.exports (get-user-menu.js:6) 23 | at tryCatch (VM12611 try-catch.js:7) 24 | at AsyncFunction.show (index.js:67) 25 | `; 26 | 27 | const result = parseError({ 28 | stack, 29 | }); 30 | 31 | const expected = [1, 2]; 32 | 33 | t.deepEqual(result, expected); 34 | t.end(); 35 | }); 36 | -------------------------------------------------------------------------------- /client/modules/user-menu/parse-user-menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {entries, assign} = Object; 4 | 5 | module.exports = (userMenu) => { 6 | const names = []; 7 | const keys = {}; 8 | const items = {}; 9 | const settings = {}; 10 | 11 | for (const [str, fn] of entries(userMenu)) { 12 | if (str === '__settings') { 13 | assign(settings, userMenu[str]); 14 | continue; 15 | } 16 | 17 | if (str.startsWith('_')) 18 | continue; 19 | 20 | names.push(str); 21 | const [key, name] = str.split(' - '); 22 | 23 | keys[key] = fn; 24 | items[name] = fn; 25 | } 26 | 27 | return { 28 | names, 29 | keys, 30 | items, 31 | settings, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /client/modules/user-menu/parse-user-menu.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const parse = require('./parse-user-menu'); 6 | 7 | test('cloudcmd: user menu: parse', (t) => { 8 | const fn = stub(); 9 | const __settings = {}; 10 | const result = parse({ 11 | __settings, 12 | 'F2 - Rename file': fn, 13 | '_f': fn, 14 | }); 15 | 16 | const names = [ 17 | 'F2 - Rename file', 18 | ]; 19 | 20 | const keys = { 21 | F2: fn, 22 | }; 23 | 24 | const items = { 25 | 'Rename file': fn, 26 | }; 27 | 28 | const expected = { 29 | names, 30 | keys, 31 | items, 32 | settings: __settings, 33 | }; 34 | 35 | t.deepEqual(result, expected); 36 | t.end(); 37 | }); 38 | -------------------------------------------------------------------------------- /client/modules/user-menu/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.runSelected = async (selectedItems, items, runUserMenu) => { 4 | for (const selected of selectedItems) { 5 | await runUserMenu(items[selected]); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /client/modules/user-menu/run.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const {runSelected} = require('./run'); 6 | 7 | test('cloudcmd: client: user menu: run', async (t) => { 8 | const runUserMenu = stub(); 9 | const fn = stub(); 10 | const selected = ['hello']; 11 | 12 | const items = { 13 | hello: fn, 14 | }; 15 | 16 | await runSelected(selected, items, runUserMenu); 17 | 18 | t.calledWith(runUserMenu, [fn]); 19 | t.end(); 20 | }); 21 | -------------------------------------------------------------------------------- /client/modules/view/get-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const currify = require('currify'); 4 | const testRegExp = currify((name, reg) => reg.test(name)); 5 | const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); 6 | 7 | const isPDF = (a) => /\.pdf$/i.test(a); 8 | const isHTML = (a) => /\.html$/.test(a); 9 | const isMarkdown = (a) => /.\.md$/.test(a); 10 | 11 | module.exports = (name) => { 12 | if (isPDF(name)) 13 | return 'pdf'; 14 | 15 | if (isImage(name)) 16 | return 'image'; 17 | 18 | if (isMedia(name)) 19 | return 'media'; 20 | 21 | if (isHTML(name)) 22 | return 'html'; 23 | 24 | if (isMarkdown(name)) 25 | return 'markdown'; 26 | }; 27 | 28 | function isImage(name) { 29 | const images = [ 30 | 'jp(e|g|eg)', 31 | 'gif', 32 | 'png', 33 | 'bmp', 34 | 'webp', 35 | 'svg', 36 | 'ico', 37 | ]; 38 | 39 | return images 40 | .map(getRegExp) 41 | .some(testRegExp(name)); 42 | } 43 | 44 | function isMedia(name) { 45 | return isAudio(name) || isVideo(name); 46 | } 47 | 48 | const isAudio = (name) => /\.(mp3|ogg|m4a)$/i.test(name); 49 | 50 | function isVideo(name) { 51 | return /\.(mp4|avi|webm)$/i.test(name); 52 | } 53 | -------------------------------------------------------------------------------- /client/modules/view/types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {extname} = require('node:path'); 4 | const currify = require('currify'); 5 | const isAudio = (name) => /\.(mp3|ogg|m4a)$/i.test(name); 6 | const testRegExp = currify((name, reg) => reg.test(name)); 7 | const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); 8 | 9 | const isPDF = (a) => /\.pdf$/i.test(a); 10 | const isHTML = (a) => /\.html$/.test(a); 11 | const isMarkdown = (a) => /.\.md$/.test(a); 12 | 13 | module.exports.getType = async (path) => { 14 | const ext = extname(path); 15 | 16 | if (!ext) 17 | path = await detectType(path); 18 | 19 | if (isPDF(path)) 20 | return 'pdf'; 21 | 22 | if (isImage(path)) 23 | return 'image'; 24 | 25 | if (isMedia(path)) 26 | return 'media'; 27 | 28 | if (isHTML(path)) 29 | return 'html'; 30 | 31 | if (isMarkdown(path)) 32 | return 'markdown'; 33 | }; 34 | 35 | module.exports.isImage = isImage; 36 | function isImage(name) { 37 | const images = [ 38 | 'jp(e|g|eg)', 39 | 'gif', 40 | 'png', 41 | 'bmp', 42 | 'webp', 43 | 'svg', 44 | 'ico', 45 | ]; 46 | 47 | return images 48 | .map(getRegExp) 49 | .some(testRegExp(name)); 50 | } 51 | 52 | function isMedia(name) { 53 | return isAudio(name) || isVideo(name); 54 | } 55 | 56 | module.exports.isAudio = isAudio; 57 | 58 | function isVideo(name) { 59 | return /\.(mp4|avi|webm)$/i.test(name); 60 | } 61 | 62 | module.exports._detectType = detectType; 63 | async function detectType(path) { 64 | const {headers} = await fetch(path, { 65 | method: 'HEAD', 66 | }); 67 | 68 | for (const [name, value] of headers) { 69 | if (name === 'content-type') 70 | return `.${value 71 | .split('/') 72 | .pop()}`; 73 | } 74 | 75 | return ''; 76 | } 77 | -------------------------------------------------------------------------------- /client/modules/view/types.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | const {isAudio, _detectType} = require('./types'); 5 | 6 | test('cloudcmd: client: view: types: isAudio', (t) => { 7 | const result = isAudio('hello.mp3'); 8 | 9 | t.ok(result); 10 | t.end(); 11 | }); 12 | 13 | test('cloudcmd: client: view: types: isAudio: no', (t) => { 14 | const result = isAudio('hello'); 15 | 16 | t.notOk(result); 17 | t.end(); 18 | }); 19 | 20 | test('cloudcmd: client: view: types: detectType', async (t) => { 21 | const fetch = stub().returns({ 22 | headers: [], 23 | }); 24 | 25 | const originalFetch = global.fetch; 26 | 27 | global.fetch = fetch; 28 | await _detectType('/hello'); 29 | 30 | global.fetch = originalFetch; 31 | const expected = ['/hello', { 32 | method: 'HEAD', 33 | }]; 34 | 35 | t.calledWith(fetch, expected); 36 | t.end(); 37 | }); 38 | 39 | test('cloudcmd: client: view: types: detectType: found', async (t) => { 40 | const originalFetch = global.fetch; 41 | 42 | global.fetch = stub().returns({ 43 | headers: [ 44 | ['content-type', 'image/png'], 45 | ], 46 | }); 47 | const result = await _detectType('/hello'); 48 | 49 | global.fetch = originalFetch; 50 | 51 | t.equal(result, '.png'); 52 | t.end(); 53 | }); 54 | -------------------------------------------------------------------------------- /client/sort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global CloudCmd */ 4 | const DOM = require('./dom'); 5 | 6 | const Info = DOM.CurrentInfo; 7 | const {sort, order} = CloudCmd; 8 | const position = DOM.getPanelPosition(); 9 | let sortPrevious = sort[position]; 10 | 11 | const {getPanel} = DOM; 12 | 13 | CloudCmd.sortPanel = (name, panel = getPanel()) => { 14 | const position = panel.dataset.name.replace('js-', ''); 15 | 16 | if (name !== sortPrevious) 17 | order[position] = 'asc'; 18 | else if (order[position] === 'asc') 19 | order[position] = 'desc'; 20 | else 21 | order[position] = 'asc'; 22 | 23 | sortPrevious = name; 24 | sort[position] = name; 25 | const noCurrent = position !== Info.panelPosition; 26 | 27 | CloudCmd.refresh({ 28 | panel, 29 | noCurrent, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /client/sw/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tryToCatch = require('try-to-catch'); 4 | 5 | module.exports.registerSW = registerSW; 6 | module.exports.unregisterSW = unregisterSW; 7 | 8 | module.exports.listenSW = (sw, ...args) => { 9 | sw?.addEventListener(...args); 10 | }; 11 | 12 | async function registerSW(prefix) { 13 | if (!navigator.serviceWorker) 14 | return; 15 | 16 | const isHTTPS = location.protocol === 'https:'; 17 | const isLocalhost = location.hostname === 'localhost'; 18 | 19 | if (!isHTTPS && !isLocalhost) 20 | return; 21 | 22 | const {serviceWorker} = navigator; 23 | const register = serviceWorker.register.bind(serviceWorker); 24 | const [e, sw] = await tryToCatch(register, `${prefix}/sw.js`); 25 | 26 | if (e) 27 | return null; 28 | 29 | return sw; 30 | } 31 | 32 | async function unregisterSW(prefix) { 33 | const reg = await registerSW(prefix); 34 | reg?.unregister(prefix); 35 | } 36 | -------------------------------------------------------------------------------- /client/sw/sw.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const codegen = require('codegen.macro'); 5 | const tryToCatch = require('try-to-catch'); 6 | const currify = require('currify'); 7 | 8 | const isDev = process.env.NODE_ENV === 'development'; 9 | 10 | const isGet = (a) => a.method === 'GET'; 11 | const isBasic = (a) => a.type === 'basic'; 12 | 13 | const wait = currify((f, e) => e.waitUntil(f())); 14 | const respondWith = currify((f, e) => { 15 | const {request} = e; 16 | const {url} = request; 17 | const pathname = getPathName(url); 18 | 19 | if (url.endsWith('/') || /\^\/fs/.test(pathname)) 20 | return; 21 | 22 | if (!isGet(request)) 23 | return; 24 | 25 | if (!isBasic(request)) 26 | return; 27 | 28 | if (pathname.startsWith('/api')) 29 | return; 30 | 31 | if (/^socket.io/.test(pathname)) 32 | return; 33 | 34 | e.respondWith(f(e)); 35 | }); 36 | 37 | const getPathName = (url) => new URL(url).pathname; 38 | 39 | const date = codegen`module.exports = '"' + Date() + '"'`; 40 | const NAME = `cloudcmd: ${date}`; 41 | 42 | const createRequest = (a) => new Request(a, { 43 | credentials: 'same-origin', 44 | }); 45 | 46 | const getRequest = (a, request) => { 47 | if (a !== '/') 48 | return request; 49 | 50 | return createRequest('/'); 51 | }; 52 | 53 | self.addEventListener('install', wait(onInstall)); 54 | self.addEventListener('fetch', respondWith(onFetch)); 55 | self.addEventListener('activate', wait(onActivate)); 56 | 57 | async function onActivate() { 58 | console.info(`cloudcmd: sw: activate: ${NAME}`); 59 | 60 | await self.clients.claim(); 61 | const keys = await caches.keys(); 62 | const deleteCache = caches.delete.bind(caches); 63 | 64 | await Promise.all(keys.map(deleteCache)); 65 | } 66 | 67 | async function onInstall() { 68 | console.info(`cloudcmd: sw: install: ${NAME}`); 69 | 70 | await self.skipWaiting(); 71 | } 72 | 73 | async function onFetch(event) { 74 | const {request} = event; 75 | const {url} = request; 76 | const pathname = getPathName(url); 77 | const newRequest = getRequest(pathname, event.request); 78 | 79 | const cache = await caches.open(NAME); 80 | const response = await cache.match(request); 81 | 82 | if (!isDev && response) 83 | return response; 84 | 85 | const [e, resp] = await tryToCatch(fetch, newRequest, { 86 | credentials: 'same-origin', 87 | }); 88 | 89 | if (e) 90 | return new Response(e.message); 91 | 92 | await addToCache(request, resp.clone()); 93 | 94 | return resp; 95 | } 96 | 97 | async function addToCache(request, response) { 98 | const cache = await caches.open(NAME); 99 | return cache.put(request, response); 100 | } 101 | -------------------------------------------------------------------------------- /common/base64.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.btoa = (str) => { 4 | if (typeof btoa === 'function') 5 | return btoa(str); 6 | 7 | return Buffer 8 | .from(str) 9 | .toString('base64'); 10 | }; 11 | 12 | module.exports.atob = (str) => { 13 | if (typeof atob === 'function') 14 | return atob(str); 15 | 16 | return Buffer 17 | .from(str, 'base64') 18 | .toString('binary'); 19 | }; 20 | -------------------------------------------------------------------------------- /common/base64.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const {btoa, atob} = require('./base64'); 6 | 7 | test('btoa: browser', (t) => { 8 | const btoaOriginal = global.btoa; 9 | const str = 'hello'; 10 | 11 | global.btoa = stub(); 12 | 13 | btoa(str); 14 | 15 | t.calledWith(global.btoa, [str], 'should call global.btoa'); 16 | t.end(); 17 | 18 | global.btoa = btoaOriginal; 19 | }); 20 | 21 | test('btoa: node', (t) => { 22 | const str = 'hello'; 23 | const expected = 'aGVsbG8='; 24 | 25 | const result = btoa(str); 26 | 27 | t.equal(result, expected, 'should encode base64'); 28 | t.end(); 29 | }); 30 | 31 | test('atob: browser', (t) => { 32 | const atobOriginal = global.atob; 33 | const str = 'hello'; 34 | 35 | global.atob = stub(); 36 | 37 | atob(str); 38 | 39 | t.calledWith(global.atob, [str], 'should call global.btoa'); 40 | t.end(); 41 | 42 | global.atob = atobOriginal; 43 | }); 44 | 45 | test('atob: node', (t) => { 46 | const str = 'aGVsbG8='; 47 | const expected = 'hello'; 48 | 49 | const result = atob(str); 50 | 51 | t.equal(result, expected, 'should encode base64'); 52 | t.end(); 53 | }); 54 | -------------------------------------------------------------------------------- /common/callbackify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const success = (f) => (data) => f(null, data); 4 | 5 | module.exports = (promise) => (...a) => { 6 | const fn = a.pop(); 7 | 8 | promise(...a) 9 | .then(success(fn)) 10 | .catch(fn); 11 | }; 12 | -------------------------------------------------------------------------------- /common/callbackify.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {promisify} = require('node:util'); 4 | const tryToCatch = require('try-to-catch'); 5 | 6 | const {test, stub} = require('supertape'); 7 | 8 | const callbackify = require('./callbackify'); 9 | 10 | test('cloudcmd: common: callbackify: error', async (t) => { 11 | const promise = stub().rejects(Error('hello')); 12 | 13 | const fn = callbackify(promise); 14 | const newPromise = promisify(fn); 15 | const [error] = await tryToCatch(newPromise); 16 | 17 | t.equal(error.message, 'hello'); 18 | t.end(); 19 | }); 20 | 21 | test('cloudcmd: common: callbackify', async (t) => { 22 | const promise = stub().resolves('hi'); 23 | 24 | const fn = callbackify(promise); 25 | const promiseAgain = promisify(fn); 26 | const data = await promiseAgain(); 27 | 28 | t.equal(data, 'hi'); 29 | t.end(); 30 | }); 31 | -------------------------------------------------------------------------------- /common/datetime.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const shortdate = require('shortdate'); 4 | 5 | module.exports = (date) => { 6 | date = date || new Date(); 7 | check(date); 8 | 9 | const timeStr = shorttime(date); 10 | const dateStr = shortdate(date); 11 | 12 | return `${dateStr} ${timeStr}`; 13 | }; 14 | 15 | const addZero = (a) => { 16 | if (a > 9) 17 | return a; 18 | 19 | return `0${a}`; 20 | }; 21 | 22 | function shorttime(date) { 23 | const seconds = addZero(date.getSeconds()); 24 | const minutes = addZero(date.getMinutes()); 25 | const hours = addZero(date.getHours()); 26 | 27 | return `${hours}:${minutes}:${seconds}`; 28 | } 29 | 30 | function check(date) { 31 | if (!(date instanceof Date)) 32 | throw Error('date should be instanceof Date!'); 33 | } 34 | -------------------------------------------------------------------------------- /common/datetime.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const tryCatch = require('try-catch'); 5 | 6 | const datetime = require('./datetime'); 7 | 8 | test('common: datetime', (t) => { 9 | const dateStr = 'Fri, 17 Aug 2018 10:56:48'; 10 | const result = datetime(new Date(dateStr)); 11 | 12 | const expected = '2018.08.17 10:56:48'; 13 | 14 | t.equal(result, expected); 15 | t.end(); 16 | }); 17 | 18 | test('common: datetime: no arg', (t) => { 19 | const {Date} = global; 20 | 21 | let called = false; 22 | 23 | global.Date = class extends Date { 24 | constructor() { 25 | super(); 26 | called = true; 27 | } 28 | }; 29 | 30 | datetime(); 31 | 32 | global.Date = Date; 33 | 34 | t.ok(called, 'should call new Date'); 35 | t.end(); 36 | }); 37 | 38 | test('common: 0 before number', (t) => { 39 | const dateStr = 'Fri, 17 Aug 2018 10:56:08'; 40 | const result = datetime(new Date(dateStr)); 41 | 42 | const expected = '2018.08.17 10:56:08'; 43 | 44 | t.equal(result, expected); 45 | t.end(); 46 | }); 47 | 48 | test('common: datetime: wrong args', (t) => { 49 | const [error] = tryCatch(datetime, {}); 50 | 51 | t.equal(error.message, 'date should be instanceof Date!', 'should throw'); 52 | t.end(); 53 | }); 54 | -------------------------------------------------------------------------------- /common/entity.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Entities = { 4 | // ' ': ' ', 5 | '<': '<', 6 | '>': '>', 7 | '"': '"', 8 | }; 9 | 10 | const keys = Object.keys(Entities); 11 | 12 | module.exports.encode = (str) => { 13 | for (const code of keys) { 14 | const char = Entities[code]; 15 | const reg = RegExp(char, 'g'); 16 | 17 | str = str.replace(reg, code); 18 | } 19 | 20 | return str; 21 | }; 22 | 23 | module.exports.decode = (str) => { 24 | for (const code of keys) { 25 | const char = Entities[code]; 26 | const reg = RegExp(code, 'g'); 27 | 28 | str = str.replace(reg, char); 29 | } 30 | 31 | return str; 32 | }; 33 | -------------------------------------------------------------------------------- /common/entity.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const entity = require('./entity'); 5 | 6 | test('cloudcmd: entity: encode', (t) => { 7 | const result = entity.encode(' '); 8 | const expected = '<hello> '; 9 | 10 | t.equal(result, expected, 'should encode entity'); 11 | t.end(); 12 | }); 13 | 14 | test('cloudcmd: entity: decode', (t) => { 15 | const result = entity.decode('<hello> '); 16 | const expected = ' '; 17 | 18 | t.equal(result, expected, 'should decode entity'); 19 | t.end(); 20 | }); 21 | 22 | test('cloudcmd: entity: encode quote', (t) => { 23 | const result = entity.encode('"hello"'); 24 | const expected = '"hello"'; 25 | 26 | t.equal(result, expected, 'should encode entity'); 27 | t.end(); 28 | }); 29 | -------------------------------------------------------------------------------- /common/try-to-promise-all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tryToCatch = require('try-to-catch'); 4 | const all = Promise.all.bind(Promise); 5 | 6 | module.exports = async (a) => { 7 | const [e, result = []] = await tryToCatch(all, a); 8 | 9 | return [ 10 | e, 11 | ...result, 12 | ]; 13 | }; 14 | -------------------------------------------------------------------------------- /common/try-to-promise-all.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const tryToPromiseAll = require('./try-to-promise-all'); 5 | 6 | const resolve = Promise.resolve.bind(Promise); 7 | const reject = Promise.reject.bind(Promise); 8 | 9 | test('commons: try-to-promise-all', async (t) => { 10 | const [, ...result] = await tryToPromiseAll([ 11 | resolve('a'), 12 | resolve('b'), 13 | ]); 14 | 15 | const expected = [ 16 | 'a', 17 | 'b', 18 | ]; 19 | 20 | t.deepEqual(result, expected); 21 | t.end(); 22 | }); 23 | 24 | test('commons: try-to-promise-all: error', async (t) => { 25 | const [e] = await tryToPromiseAll([ 26 | reject('a'), 27 | ]); 28 | 29 | t.equal(e, 'a'); 30 | t.end(); 31 | }); 32 | -------------------------------------------------------------------------------- /common/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const exec = require('execon'); 4 | const isString = (a) => typeof a === 'string'; 5 | 6 | module.exports.escapeRegExp = (str) => { 7 | const isStr = isString(str); 8 | 9 | if (isStr) 10 | str = str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 11 | 12 | return str; 13 | }; 14 | 15 | /** 16 | * get regexp from wild card 17 | */ 18 | module.exports.getRegExp = (wildcard) => { 19 | const escaped = `^${wildcard // search from start of line 20 | .replace(/\./g, '\\.') 21 | .replace(/\*/g, '.*') 22 | .replace('?', '.?')}$`; 23 | 24 | // search to end of line 25 | return RegExp(escaped); 26 | }; 27 | 28 | module.exports.exec = exec; 29 | /** 30 | * function gets file extension 31 | * 32 | * @param name 33 | * @return ext 34 | */ 35 | module.exports.getExt = (name) => { 36 | const isStr = isString(name); 37 | 38 | if (!isStr) 39 | return ''; 40 | 41 | const dot = name.lastIndexOf('.'); 42 | 43 | if (~dot) 44 | return name.substr(dot); 45 | 46 | return ''; 47 | }; 48 | 49 | /** 50 | * find object by name in arrray 51 | * 52 | * @param array 53 | * @param name 54 | */ 55 | module.exports.findObjByNameInArr = (array, name) => { 56 | let ret; 57 | 58 | if (!Array.isArray(array)) 59 | throw Error('array should be array!'); 60 | 61 | if (!isString(name)) 62 | throw Error('name should be string!'); 63 | 64 | array.some((item) => { 65 | const is = item.name === name; 66 | const isArray = Array.isArray(item); 67 | 68 | if (is) { 69 | ret = item; 70 | return is; 71 | } 72 | 73 | if (!isArray) 74 | return is; 75 | 76 | return item.some((item) => { 77 | const is = item.name === name; 78 | 79 | if (is) 80 | ret = item.data; 81 | 82 | return is; 83 | }); 84 | }); 85 | 86 | return ret; 87 | }; 88 | 89 | /** 90 | * start timer 91 | * @param name 92 | */ 93 | module.exports.time = (name) => { 94 | exec.ifExist(console, 'time', [name]); 95 | }; 96 | 97 | /** 98 | * stop timer 99 | * @param name 100 | */ 101 | module.exports.timeEnd = (name) => { 102 | exec.ifExist(console, 'timeEnd', [name]); 103 | }; 104 | -------------------------------------------------------------------------------- /css/columns/name-size-date.css: -------------------------------------------------------------------------------- 1 | .name { 2 | width: 55%; 3 | } 4 | 5 | .size { 6 | float: none; 7 | } 8 | 9 | .owner { 10 | display: none; 11 | } 12 | 13 | .mode { 14 | display: none; 15 | } 16 | 17 | .date { 18 | float: right; 19 | width: 19%; 20 | } 21 | -------------------------------------------------------------------------------- /css/columns/name-size.css: -------------------------------------------------------------------------------- 1 | .name { 2 | width: 77%; 3 | } 4 | 5 | .size { 6 | float: none; 7 | margin-right: 0; 8 | } 9 | 10 | .owner { 11 | display: none; 12 | } 13 | 14 | .mode { 15 | display: none; 16 | } 17 | 18 | .date { 19 | display: none; 20 | } 21 | -------------------------------------------------------------------------------- /css/config.css: -------------------------------------------------------------------------------- 1 | .config { 2 | white-space: normal; 3 | overflow: hidden; 4 | width: 250px; 5 | } 6 | 7 | .list li { 8 | list-style-type: none; 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | -o-user-select: none; 13 | user-select: none; 14 | } 15 | 16 | .config .form-control { 17 | width: 200px; 18 | padding: 0 12px; 19 | font-size: 16px; 20 | line-height: 1.5; 21 | color: var(--column-color); 22 | background: var(--internal-background); 23 | border: 1px solid #ccc; 24 | -webkit-box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); 25 | -moz-box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); 26 | box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); 27 | -webkit-transition: 28 | border-color ease-in-out 0.15s, 29 | box-shadow ease-in-out 0.15s; 30 | -moz-transition: 31 | border-color ease-in-out 0.15s, 32 | box-shadow ease-in-out 0.15s; 33 | -o-transition: 34 | border-color ease-in-out 0.15s, 35 | box-shadow ease-in-out 0.15s; 36 | transition: 37 | border-color ease-in-out 0.15s, 38 | box-shadow ease-in-out 0.15s; 39 | } 40 | 41 | .config .form-control::-moz-placeholder { 42 | color: #999; 43 | } 44 | 45 | .config .form-control::-ms-input-placeholder { 46 | color: #999; 47 | } 48 | 49 | .config .form-control::-webkit-input-placeholder { 50 | color: #999; 51 | } 52 | 53 | .config .form-control:focus { 54 | border-color: #66afe9; 55 | outline: 0; 56 | -webkit-box-shadow: 57 | inset 0 1px 1px rgb(0 0 0 / 7.5%), 58 | 0 0 1px rgb(102 175 233 / 60%); 59 | -moz-box-shadow: 60 | inset 0 1px 1px rgb(0 0 0 / 7.5%), 61 | 0 0 1px rgb(102 175 233 / 60%); 62 | box-shadow: 63 | inset 0 1px 1px rgb(0 0 0 / 7.5%), 64 | 0 0 1px rgb(102 175 233 / 60%); 65 | } 66 | 67 | .config .form-control:focus:invalid:focus { 68 | border-color: #e9322d; 69 | -webkit-box-shadow: 0 0 6px #f8b9b7; 70 | -moz-box-shadow: 0 0 6px #f8b9b7; 71 | box-shadow: 0 0 6px #f8b9b7; 72 | } 73 | 74 | .config .list { 75 | padding: 0; 76 | margin: 5%; 77 | } 78 | 79 | .config .full-width { 80 | width: 100%; 81 | } 82 | -------------------------------------------------------------------------------- /css/help.css: -------------------------------------------------------------------------------- 1 | .help { 2 | margin: 25px; 3 | white-space: normal; 4 | } 5 | 6 | .help li { 7 | list-style-type: disc; 8 | } 9 | 10 | .help img { 11 | max-width: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /css/icons.css: -------------------------------------------------------------------------------- 1 | .icon-help::before { 2 | font-family: 'Fontello'; 3 | content: '\e801 '; 4 | } 5 | 6 | .icon-rename::before { 7 | font-family: 'Fontello'; 8 | content: '\e802 '; 9 | } 10 | 11 | .icon-view::before { 12 | font-family: 'Fontello'; 13 | content: '\e803 '; 14 | } 15 | 16 | .icon-edit::before { 17 | font-family: 'Fontello'; 18 | content: '\e804 '; 19 | } 20 | 21 | .icon-copy::before { 22 | font-family: 'Fontello'; 23 | content: '\e805 '; 24 | } 25 | 26 | .icon-move::before { 27 | font-family: 'Fontello'; 28 | content: '\e806 '; 29 | } 30 | 31 | .icon-directory::before { 32 | font-family: 'Fontello'; 33 | content: '\e807 '; 34 | } 35 | 36 | .icon-delete::before { 37 | font-family: 'Fontello'; 38 | content: '\e808 '; 39 | } 40 | 41 | .icon-menu::before { 42 | font-family: 'Fontello'; 43 | content: '\e809 '; 44 | } 45 | 46 | .icon-config::before { 47 | font-family: 'Fontello'; 48 | content: '\e80a '; 49 | } 50 | 51 | .icon-console::before { 52 | font-family: 'Fontello'; 53 | content: '\e80b '; 54 | } 55 | 56 | .icon-contact::before { 57 | font-family: 'Fontello'; 58 | content: '\e80c '; 59 | } 60 | 61 | .icon-file::before { 62 | font-family: 'Fontello'; 63 | content: '\e80d '; 64 | } 65 | 66 | .icon-upload-to-cloud::before { 67 | font-family: 'Fontello'; 68 | content: '\e80e '; 69 | } 70 | 71 | .icon-upload-from-cloud::before { 72 | font-family: 'Fontello'; 73 | content: '\e80f '; 74 | } 75 | 76 | .icon-download::before { 77 | font-family: 'Fontello'; 78 | content: '\e810 '; 79 | } 80 | 81 | .icon-new::before { 82 | font-family: 'Fontello'; 83 | content: '\e811 '; 84 | } 85 | 86 | .icon-toggle-file-selection::before { 87 | font-family: 'Fontello'; 88 | content: '\e81f '; 89 | } 90 | 91 | .icon-unselect-all::before { 92 | font-family: 'Fontello'; 93 | content: '\e812 '; 94 | } 95 | 96 | .icon-pack::before { 97 | font-family: 'Fontello'; 98 | content: '\e813 '; 99 | } 100 | 101 | .icon-extract::before { 102 | font-family: 'Fontello'; 103 | content: '\e814 '; 104 | } 105 | 106 | .icon-copy-to-clipboard::before { 107 | font-family: 'Fontello'; 108 | content: '\e815 '; 109 | } 110 | 111 | .icon-refresh::before { 112 | font-family: 'Fontello'; 113 | content: '\e816 '; 114 | } 115 | 116 | .icon-cut::before { 117 | font-family: 'Fontello'; 118 | content: '\e817 '; 119 | } 120 | 121 | .icon-paste::before { 122 | font-family: 'Fontello'; 123 | content: '\e818 '; 124 | } 125 | 126 | .icon-upload::before { 127 | font-family: 'Fontello'; 128 | content: '\e819 '; 129 | } 130 | 131 | .icon-log-out::before { 132 | font-family: 'Fontello'; 133 | content: '\e81a '; 134 | } 135 | 136 | .icon-terminal::before { 137 | font-family: 'Fontello'; 138 | content: '\e81b '; 139 | } 140 | 141 | .icon-user-menu::before { 142 | font-family: 'Fontello'; 143 | content: '\e81c '; 144 | } 145 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | @import url(./urls.css); 2 | @import url(./reset.css); 3 | @import url(./style.css); 4 | @import url(./icons.css); 5 | @import url(./help.css); 6 | @import url(./query.css); 7 | @import url(./supports.css); 8 | -------------------------------------------------------------------------------- /css/nojs.css: -------------------------------------------------------------------------------- 1 | .path-icon, 2 | .keyspanel { 3 | display: none; 4 | } 5 | 6 | .links { 7 | margin-left: 16px; 8 | } 9 | 10 | .fm { 11 | height: 100%; 12 | } 13 | 14 | .panel-right { 15 | display: none; 16 | } 17 | 18 | .panel-left { 19 | width: 98%; 20 | } 21 | 22 | .name a:hover { 23 | cursor: pointer; 24 | } 25 | -------------------------------------------------------------------------------- /css/reset.css: -------------------------------------------------------------------------------- 1 | /* ============================================================================= 2 | Base 3 | ========================================================================== */ 4 | 5 | /* 6 | * 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units 7 | * 2. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g 8 | */ 9 | 10 | html { 11 | color: #222; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | } 17 | -------------------------------------------------------------------------------- /css/supports.css: -------------------------------------------------------------------------------- 1 | @supports (overflow: overlay) { 2 | .files { 3 | overflow-y: auto; 4 | } 5 | 6 | .fm-header { 7 | overflow-y: hidden; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /css/terminal.css: -------------------------------------------------------------------------------- 1 | .terminal { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /css/themes/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --link-color: #317bf9; 3 | --border-color: rgb(49 123 249 / 40%); 4 | --background: #22272e; 5 | --column-color: #727e8c; 6 | --icon-color: #478be6; 7 | --button-background: #22272e; 8 | --internal-background: #373e47; 9 | } 10 | 11 | .view { 12 | background: var(--internal-background) !important; 13 | color: var(--column-color) !important; 14 | } 15 | .view a { 16 | color: var(--link-color) !important; 17 | } 18 | 19 | .smalltalk .page, 20 | .smalltalk header, 21 | .smalltalk .button-strip button, 22 | .smalltalk input { 23 | background: var(--internal-background) !important; 24 | color: var(--link-color) !important; 25 | text-shadow: none !important; 26 | } 27 | 28 | .cloudcmd-user-menu, 29 | .cloudcmd-user-menu-button { 30 | background: var(--internal-background) !important; 31 | color: var(--link-color) !important; 32 | } 33 | 34 | .jqconsole { 35 | background: #373e47 !important; 36 | } 37 | 38 | .jqconsole-prompt { 39 | color: var(--column-color) !important; 40 | } 41 | 42 | .log-msg { 43 | color: var(--column-color) !important; 44 | } 45 | 46 | .menu { 47 | color: var(--link-color) !important; 48 | background: var(--internal-background) !important; 49 | } 50 | -------------------------------------------------------------------------------- /css/themes/light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --link-color: blue; 3 | --selected-menu-item-color: #317bf9; 4 | --border-color: rgb(49 123 249 / 40%); 5 | --background: white; 6 | --column-color: black; 7 | --icon-color: #222; 8 | --button-background: white; 9 | } 10 | -------------------------------------------------------------------------------- /css/urls.css: -------------------------------------------------------------------------------- 1 | /* 2 | @import url(//fonts.googleapis.com/css?family=Droid+Sans+Mono); 3 | */ 4 | 5 | /* http://fontello.com/ */ 6 | @font-face { 7 | font-family: 'Fontello'; 8 | src: url(../font/fontello.eot); 9 | src: 10 | url(../font/fontello.eot?#iefix) format('embedded-opentype'), 11 | url(../font/fontello.woff2) format('woff2'), 12 | url(../font/fontello.woff) format('woff'), 13 | url(../font/fontello.ttf) format('truetype'), 14 | url(../font/fontello.svg#cloudcmd) format('svg'); 15 | font-weight: normal; 16 | font-style: normal; 17 | } 18 | 19 | @font-face { 20 | font-family: 'Droid Sans Mono'; 21 | src: url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot); 22 | src: 23 | local('Droid Sans Mono'), 24 | local('DroidSansMono'), 25 | url(../font/DroidSansMono.eot) format('embedded-opentype'), 26 | url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot?#iefix) 27 | format('embedded-opentype'), 28 | url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot) 29 | format('embedded-opentype'), 30 | url(../font/DroidSansMono.woff2) format('woff2'), 31 | url(../font/DroidSansMono.woff) format('woff'), 32 | url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff) 33 | format('woff'), 34 | local('Consolas'); 35 | font-style: normal; 36 | font-weight: 400; 37 | } 38 | 39 | .directory { 40 | background-image: url(../img/directory.png); 41 | } 42 | 43 | .directory-link { 44 | background-image: url(../img/directory-link.png); 45 | } 46 | 47 | .file { 48 | background-image: url(../img/file.png); 49 | } 50 | 51 | .file-link { 52 | background-image: url(../img/file-link.png); 53 | } 54 | 55 | .archive { 56 | background-image: url(../img/archive.png); 57 | } 58 | 59 | .archive-link { 60 | background-image: url(../img/archive-link.png); 61 | } 62 | 63 | .loading-svg { 64 | background: url(../img/spinner.svg); 65 | } 66 | 67 | .loading-gif { 68 | background: url(../img/spinner.gif); 69 | } 70 | -------------------------------------------------------------------------------- /css/user-menu.css: -------------------------------------------------------------------------------- 1 | .cloudcmd-user-menu { 2 | font-size: 16px; 3 | font-family: 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; 4 | border: 0; 5 | } 6 | 7 | .cloudcmd-user-menu:focus { 8 | outline: 0; 9 | } 10 | 11 | .cloudcmd-user-menu > option:checked { 12 | box-shadow: 20px -20px 0 2px var(--selected-menu-item-color) inset; 13 | } 14 | 15 | .cloudcmd-user-menu-button { 16 | display: block; 17 | width: 100%; 18 | font-size: 16px; 19 | padding: 2px; 20 | -webkit-appearance: none; 21 | border: 0; 22 | overflow: auto; 23 | } 24 | -------------------------------------------------------------------------------- /css/view.css: -------------------------------------------------------------------------------- 1 | .view { 2 | font-size: 16px; 3 | white-space: pre; 4 | height: 100%; 5 | overflow: auto; 6 | } 7 | 8 | .view:focus { 9 | outline: 0; 10 | } 11 | 12 | .view::selection { 13 | text-shadow: none; 14 | background: #b3d4fc; 15 | } 16 | 17 | .view-overlay { 18 | display: block; 19 | background: rgb(255 255 255 / 10%); 20 | } 21 | 22 | .media, 23 | video { 24 | width: 100%; 25 | } 26 | 27 | audio { 28 | margin-top: 10px; 29 | } 30 | -------------------------------------------------------------------------------- /cssnano.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // used by OptimizeCssAssetsPlugin 4 | const defaultPreset = require('cssnano-preset-default'); 5 | 6 | module.exports = defaultPreset({ 7 | svgo: { 8 | plugins: [{ 9 | convertPathData: false, 10 | }, { 11 | convertShapeToPath: false, 12 | }], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | web: 4 | ports: 5 | - 8000:8000 6 | volumes: 7 | - ~:/root 8 | - /:/mnt/fs 9 | image: coderaiser/cloudcmd 10 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-bookworm 2 | LABEL maintainer="Coderaiser" 3 | LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" 4 | 5 | RUN mkdir -p /usr/src/app 6 | WORKDIR /usr/src/app 7 | 8 | COPY package.json /usr/src/app/ 9 | 10 | RUN npm config set package-lock false && \ 11 | npm install --production && \ 12 | npm i gritty && \ 13 | npm cache clean --force 14 | 15 | COPY . /usr/src/app 16 | 17 | WORKDIR / 18 | 19 | ENV cloudcmd_terminal=true 20 | ENV cloudcmd_terminal_path=gritty 21 | ENV cloudcmd_open=false 22 | 23 | EXPOSE 8000 24 | 25 | ENTRYPOINT ["/usr/src/app/bin/cloudcmd.mjs"] 26 | -------------------------------------------------------------------------------- /docker/Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | LABEL maintainer="Coderaiser" 3 | LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" 4 | 5 | RUN mkdir -p /usr/src/app 6 | WORKDIR /usr/src/app 7 | 8 | COPY package.json /usr/src/app/ 9 | 10 | RUN npm config set package-lock false && \ 11 | npm install --production && \ 12 | apk update && \ 13 | apk add --no-cache bash make g++ python3 && \ 14 | npm i gritty && \ 15 | npm cache clean --force && \ 16 | apk del make g++ python3 && \ 17 | rm -rf /usr/include /tmp/* /var/cache/apk/* 18 | 19 | COPY . /usr/src/app 20 | 21 | WORKDIR / 22 | 23 | ENV cloudcmd_terminal true 24 | ENV cloudcmd_terminal_path gritty 25 | ENV cloudcmd_open false 26 | 27 | EXPOSE 8000 28 | 29 | ENTRYPOINT ["/usr/src/app/bin/cloudcmd.mjs"] 30 | 31 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/favicon.ico -------------------------------------------------------------------------------- /font/DroidSansMono.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/font/DroidSansMono.eot -------------------------------------------------------------------------------- /font/DroidSansMono.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/font/DroidSansMono.woff -------------------------------------------------------------------------------- /font/DroidSansMono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/font/DroidSansMono.woff2 -------------------------------------------------------------------------------- /font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/font/fontello.eot -------------------------------------------------------------------------------- /font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/font/fontello.ttf -------------------------------------------------------------------------------- /font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/font/fontello.woff -------------------------------------------------------------------------------- /font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/font/fontello.woff2 -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 22 | 23 | 24 |
{{ fm }}
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /img/archive-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/archive-link.png -------------------------------------------------------------------------------- /img/archive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/archive.png -------------------------------------------------------------------------------- /img/directory-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/directory-link.png -------------------------------------------------------------------------------- /img/directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/directory.png -------------------------------------------------------------------------------- /img/favicon/favicon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/favicon/favicon-256.png -------------------------------------------------------------------------------- /img/favicon/favicon-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/favicon/favicon-big.png -------------------------------------------------------------------------------- /img/favicon/favicon-notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/favicon/favicon-notify.png -------------------------------------------------------------------------------- /img/favicon/favicon.cdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/favicon/favicon.cdr -------------------------------------------------------------------------------- /img/favicon/favicon.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/favicon/favicon.eps -------------------------------------------------------------------------------- /img/favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/favicon/favicon.png -------------------------------------------------------------------------------- /img/file-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/file-link.png -------------------------------------------------------------------------------- /img/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/file.png -------------------------------------------------------------------------------- /img/logo/cloudcmd-hq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/logo/cloudcmd-hq.png -------------------------------------------------------------------------------- /img/logo/cloudcmd.cdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/logo/cloudcmd.cdr -------------------------------------------------------------------------------- /img/logo/cloudcmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/logo/cloudcmd.png -------------------------------------------------------------------------------- /img/screen/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/screen/config.png -------------------------------------------------------------------------------- /img/screen/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/screen/console.png -------------------------------------------------------------------------------- /img/screen/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/screen/edit.png -------------------------------------------------------------------------------- /img/screen/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/screen/menu.png -------------------------------------------------------------------------------- /img/screen/one-file-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/screen/one-file-panel.png -------------------------------------------------------------------------------- /img/screen/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/screen/terminal.png -------------------------------------------------------------------------------- /img/screen/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/screen/view.png -------------------------------------------------------------------------------- /img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/img/spinner.gif -------------------------------------------------------------------------------- /img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /json/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "auth": false, 4 | "username": "root", 5 | "password": "2b64f2e3f9fee1942af9ff60d40aa5a719db33b8ba8dd4864bb4f11e25ca2bee00907de32a59429602336cac832c8f2eeff5177cc14c864dd116c8bf6ca5d9a9", 6 | "algo": "sha512WithRSAEncryption", 7 | "editor": "edward", 8 | "packer": "tar", 9 | "diff": true, 10 | "zip": true, 11 | "buffer": true, 12 | "dirStorage": false, 13 | "online": false, 14 | "open": true, 15 | "keysPanel": true, 16 | "port": 8000, 17 | "ip": null, 18 | "root": "/", 19 | "prefix": "", 20 | "prefixSocket": "", 21 | "contact": true, 22 | "confirmCopy": true, 23 | "confirmMove": true, 24 | "configDialog": true, 25 | "configAuth": true, 26 | "oneFilePanel": false, 27 | "console": true, 28 | "syncConsolePath": false, 29 | "terminal": false, 30 | "terminalPath": "", 31 | "terminalCommand": "", 32 | "terminalAutoRestart": true, 33 | "showDotFiles": true, 34 | "showConfig": false, 35 | "showFileName": false, 36 | "vim": false, 37 | "columns": "name-size-date-owner-mode", 38 | "theme": "light", 39 | "export": false, 40 | "exportToken": "root", 41 | "import": false, 42 | "importToken": "root", 43 | "importUrl": "http://localhost:8000", 44 | "importListen": false, 45 | "log": true, 46 | "dropbox": false, 47 | "dropboxToken": "" 48 | } 49 | -------------------------------------------------------------------------------- /json/modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": [ 3 | "edit", 4 | "edit-file", 5 | "edit-file-vim", 6 | "edit-names", 7 | "edit-names-vim", 8 | "menu", 9 | "view", 10 | "help", 11 | "markdown", 12 | "config", 13 | "contact", 14 | "command-line", 15 | "upload", 16 | "operation", 17 | "konsole", 18 | "terminal", 19 | "terminal-run", 20 | "cloud", 21 | "user-menu" 22 | ], 23 | "remote": [{ 24 | "name": "socket", 25 | "version": "4.0.1", 26 | "local": "/socket.io/socket.io.js", 27 | "remote": "https://cdnjs.cloudflare.com/ajax/libs/socket.io/{{ version }}/socket.io.js" 28 | }], 29 | "data": { 30 | "FilePicker": { 31 | "key": "AACq5fTfzRY2E_Rw_4kyaz" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | applications: 2 | .: 3 | name: cloudcmd 4 | framework: 5 | name: node 6 | info: 7 | mem: 512M 8 | description: Node.js Application 9 | exec: null 10 | url: ${name}.${target-base} 11 | mem: 128M 12 | instances: 2 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CloudCMD", 3 | "name": "Cloud Commander", 4 | "display": "standalone", 5 | "start_url": "..", 6 | "icons": [{ 7 | "src": "../img/favicon/favicon-256.png", 8 | "type": "image/png", 9 | "sizes": "256x256" 10 | }] 11 | } 12 | -------------------------------------------------------------------------------- /server/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const httpAuth = require('http-auth'); 4 | const criton = require('criton'); 5 | const currify = require('currify'); 6 | const middle = currify(_middle); 7 | const check = currify(_check); 8 | 9 | module.exports = (config) => { 10 | const auth = httpAuth.basic({ 11 | realm: 'Cloud Commander', 12 | }, check(config)); 13 | 14 | return middle(config, auth); 15 | }; 16 | 17 | function _middle(config, authentication, req, res, next) { 18 | const is = config('auth'); 19 | const {originalUrl} = req; 20 | 21 | if (!is || originalUrl.startsWith('/public/')) 22 | return next(); 23 | 24 | const success = () => next(); 25 | 26 | return authentication.check(success)(req, res); 27 | } 28 | 29 | function _check(config, username, password, callback) { 30 | const BAD_CREDENTIALS = false; 31 | const name = config('username'); 32 | const pass = config('password'); 33 | const algo = config('algo'); 34 | 35 | if (!password) 36 | return callback(BAD_CREDENTIALS); 37 | 38 | const sameName = username === name; 39 | const samePass = pass === criton(password, algo); 40 | 41 | callback(sameName && samePass); 42 | } 43 | -------------------------------------------------------------------------------- /server/columns.mjs: -------------------------------------------------------------------------------- 1 | import path, {dirname} from 'node:path'; 2 | import {fileURLToPath} from 'node:url'; 3 | import process from 'node:process'; 4 | import fs from 'node:fs'; 5 | import fullstore from 'fullstore'; 6 | import nanomemoizeDefault from 'nano-memoize'; 7 | import readFilesSync from '@cloudcmd/read-files-sync'; 8 | 9 | const {nanomemoize} = nanomemoizeDefault; 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | const isMap = (a) => /\.map$/.test(a); 13 | const not = (fn) => (a) => !fn(a); 14 | 15 | const defaultColumns = { 16 | '': '', 17 | 'name-size-date-owner-mode': '', 18 | }; 19 | 20 | const _isDev = fullstore(process.env.NODE_ENV === 'development'); 21 | const getDist = (isDev) => isDev ? 'dist-dev' : 'dist'; 22 | 23 | export const isDev = _isDev; 24 | 25 | export const getColumns = ({isDev = _isDev()} = {}) => { 26 | const columns = readFilesSyncMemo(isDev); 27 | 28 | return { 29 | ...columns, 30 | ...defaultColumns, 31 | }; 32 | }; 33 | 34 | const readFilesSyncMemo = nanomemoize((isDev) => { 35 | const dist = getDist(isDev); 36 | const columnsDir = path.join(__dirname, '..', dist, 'columns'); 37 | const names = fs 38 | .readdirSync(columnsDir) 39 | .filter(not(isMap)); 40 | 41 | return readFilesSync(columnsDir, names, 'utf8'); 42 | }); 43 | -------------------------------------------------------------------------------- /server/columns.spec.mjs: -------------------------------------------------------------------------------- 1 | import {dirname} from 'node:path'; 2 | import {fileURLToPath} from 'node:url'; 3 | import fs from 'node:fs'; 4 | import test from 'supertape'; 5 | import {getColumns, isDev} from './columns.mjs'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | test('columns: prod', (t) => { 11 | const columns = getColumns({ 12 | isDev: false, 13 | }); 14 | 15 | t.equal(columns[''], ''); 16 | t.end(); 17 | }); 18 | 19 | test('columns: dev', (t) => { 20 | const columns = getColumns({ 21 | isDev: true, 22 | }); 23 | 24 | const css = fs.readFileSync(`${__dirname}/../css/columns/name-size-date.css`, 'utf8'); 25 | 26 | t.equal(columns['name-size-date'], css); 27 | t.end(); 28 | }); 29 | 30 | test('columns: no args', (t) => { 31 | const currentIsDev = isDev(); 32 | isDev(true); 33 | const columns = getColumns(); 34 | 35 | const css = fs.readFileSync(`${__dirname}/../css/columns/name-size-date.css`, 'utf8'); 36 | isDev(currentIsDev); 37 | 38 | t.equal(columns['name-size-date'], css); 39 | t.end(); 40 | }); 41 | -------------------------------------------------------------------------------- /server/config.fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "password": "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" 3 | } 4 | -------------------------------------------------------------------------------- /server/config.spec.mjs: -------------------------------------------------------------------------------- 1 | import {createRequire} from 'node:module'; 2 | import {test, stub} from 'supertape'; 3 | import {createConfig, _cryptoPass} from './config.js'; 4 | import {apiURL} from '../common/cloudfunc.js'; 5 | import {connect} from '../test/before.mjs'; 6 | 7 | const require = createRequire(import.meta.url); 8 | const fixture = require('./config.fixture.json'); 9 | 10 | const config = createConfig(); 11 | 12 | test('config: manage', (t) => { 13 | t.equal(undefined, config(), 'should return "undefined"'); 14 | t.end(); 15 | }); 16 | 17 | test('config: manage: get', async (t) => { 18 | const editor = 'deepword'; 19 | const configManager = createConfig(); 20 | 21 | const {done} = await connect({ 22 | config: { 23 | editor, 24 | }, 25 | configManager, 26 | }); 27 | 28 | done(); 29 | 30 | t.equal(configManager('editor'), editor, 'should get config'); 31 | t.end(); 32 | }); 33 | 34 | test('config: manage: get: config', async (t) => { 35 | const editor = 'deepword'; 36 | const conf = { 37 | editor, 38 | }; 39 | 40 | const {done} = await connect({ 41 | config: conf, 42 | }); 43 | 44 | config('editor', 'dword'); 45 | done(); 46 | 47 | t.equal('dword', config('editor'), 'should set config'); 48 | t.end(); 49 | }); 50 | 51 | test('config: manage: get: *', (t) => { 52 | const data = config('*'); 53 | const keys = Object.keys(data); 54 | 55 | t.ok(keys.length > 1, 'should return config data'); 56 | t.end(); 57 | }); 58 | 59 | test('config: cryptoPass: no password', (t) => { 60 | const json = { 61 | hello: 'world', 62 | }; 63 | 64 | const config = createConfig(); 65 | const result = _cryptoPass(config, json); 66 | 67 | t.deepEqual(result, [config, json], 'should not change json'); 68 | t.end(); 69 | }); 70 | 71 | test('config: cryptoPass', (t) => { 72 | const json = { 73 | password: 'hello', 74 | }; 75 | 76 | const {password} = fixture; 77 | 78 | const expected = { 79 | password, 80 | }; 81 | 82 | const config = createConfig(); 83 | const result = _cryptoPass(config, json); 84 | 85 | t.deepEqual(result, [config, expected], 'should crypt password'); 86 | t.end(); 87 | }); 88 | 89 | test('config: middle: no', (t) => { 90 | const {middle} = config; 91 | const next = stub(); 92 | const res = null; 93 | const url = `${apiURL}/config`; 94 | const method = 'POST'; 95 | 96 | const req = { 97 | url, 98 | method, 99 | }; 100 | 101 | middle(req, res, next); 102 | 103 | t.calledWithNoArgs(next, 'should call next'); 104 | t.end(); 105 | }); 106 | -------------------------------------------------------------------------------- /server/depstore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.createDepStore = () => { 4 | let deps = {}; 5 | 6 | return (name, value) => { 7 | if (!name) 8 | return deps = {}; 9 | 10 | if (!value) 11 | return deps[name]; 12 | 13 | deps[name] = value; 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /server/distribute/export.spec.mjs: -------------------------------------------------------------------------------- 1 | import {once} from 'node:events'; 2 | import test from 'supertape'; 3 | import io from 'socket.io-client'; 4 | import Config from '../config.js'; 5 | import {connect} from '../../test/before.mjs'; 6 | 7 | const config = Config.createConfig(); 8 | 9 | test('distribute: export', async (t) => { 10 | const defaultConfig = { 11 | export: true, 12 | exportToken: 'a', 13 | vim: true, 14 | log: false, 15 | prefix: '', 16 | }; 17 | 18 | const {port, done} = await connect({ 19 | config: defaultConfig, 20 | configManager: config, 21 | }); 22 | 23 | const url = `http://localhost:${port}/distribute?port=1111`; 24 | const socket = io.connect(url); 25 | 26 | await once(socket, 'connect'); 27 | socket.emit('auth', 'a'); 28 | 29 | await once(socket, 'accept'); 30 | config('vim', false); 31 | config('auth', true); 32 | 33 | await once(socket, 'change'); 34 | 35 | socket.close(); 36 | await done(); 37 | 38 | t.pass('should emit change'); 39 | t.end(); 40 | }); 41 | 42 | test('distribute: export: config', async (t) => { 43 | const defaultConfig = { 44 | export: true, 45 | exportToken: 'a', 46 | vim: true, 47 | log: false, 48 | }; 49 | 50 | const {port, done} = await connect({ 51 | config: defaultConfig, 52 | }); 53 | 54 | const url = `http://localhost:${port}/distribute?port=1111`; 55 | const socket = io.connect(url); 56 | 57 | socket.once('connect', () => { 58 | socket.emit('auth', 'a'); 59 | }); 60 | 61 | const data = await once(socket, 'config'); 62 | 63 | socket.close(); 64 | await done(); 65 | 66 | t.equal(typeof data, 'object', 'should emit object'); 67 | t.end(); 68 | }); 69 | -------------------------------------------------------------------------------- /server/distribute/log.mjs: -------------------------------------------------------------------------------- 1 | import wraptile from 'wraptile'; 2 | import chalk from 'chalk'; 3 | import datetime from '../../common/datetime.js'; 4 | 5 | const {assign} = Object; 6 | 7 | const log = (isLog, name, msg) => isLog && console.log(`${datetime()} -> ${name}: ${msg}`); 8 | 9 | export const makeColor = (a) => chalk.blue(a); 10 | export const getMessage = (e) => e.message || e; 11 | export const getDescription = (e) => e.message; 12 | 13 | export default log; 14 | 15 | export const logWrapped = wraptile(log); 16 | 17 | export const importStr = 'import'; 18 | export const exportStr = 'export'; 19 | export const connectedStr = chalk.green('connected'); 20 | export const disconnectedStr = chalk.red('disconnected'); 21 | export const tokenRejectedStr = chalk.red('token rejected'); 22 | export const authTryStr = chalk.yellow('try to auth'); 23 | 24 | export function stringToRGB(a) { 25 | return [ 26 | a.charCodeAt(0), 27 | a.length, 28 | crc(a), 29 | ]; 30 | } 31 | 32 | const add = (a, b) => a + b.charCodeAt(0); 33 | 34 | function crc(a) { 35 | return a 36 | .split('') 37 | .reduce(add, 0); 38 | } 39 | 40 | assign(log, { 41 | getMessage, 42 | makeColor, 43 | getDescription, 44 | authTryStr, 45 | stringToRGB, 46 | logWrapped, 47 | importStr, 48 | exportStr, 49 | connectedStr, 50 | disconnectedStr, 51 | tokenRejectedStr, 52 | }); 53 | -------------------------------------------------------------------------------- /server/distribute/log.spec.mjs: -------------------------------------------------------------------------------- 1 | import test from 'supertape'; 2 | import log from './log.mjs'; 3 | import {createConfig} from '../config.js'; 4 | 5 | test('distribute: log: getMessage', (t) => { 6 | const e = 'hello'; 7 | const result = log.getMessage(e); 8 | 9 | t.equal(e, result); 10 | t.end(); 11 | }); 12 | 13 | test('distribute: log: getMessage: message', (t) => { 14 | const message = 'hello'; 15 | const result = log.getMessage({ 16 | message, 17 | }); 18 | 19 | t.equal(result, message); 20 | t.end(); 21 | }); 22 | 23 | test('distribute: log: config', (t) => { 24 | const config = createConfig(); 25 | const logOriginal = config('log'); 26 | 27 | config('log', true); 28 | log('log', 'test message'); 29 | config('log', logOriginal); 30 | 31 | t.end(); 32 | }, { 33 | checkAssertionsCount: false, 34 | }); 35 | -------------------------------------------------------------------------------- /server/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {env} = require('node:process'); 4 | const snake = require('just-snake-case'); 5 | 6 | const up = (a) => a.toUpperCase(); 7 | 8 | module.exports = parse; 9 | module.exports.bool = (name) => { 10 | const value = parse(name); 11 | 12 | if (value === 'true') 13 | return true; 14 | 15 | if (value === '1') 16 | return true; 17 | 18 | if (value === 'false') 19 | return false; 20 | 21 | if (value === '0') 22 | return false; 23 | }; 24 | 25 | function parse(name) { 26 | const small = `cloudcmd_${snake(name)}`; 27 | const big = up(small); 28 | 29 | return env[big] || env[small]; 30 | } 31 | -------------------------------------------------------------------------------- /server/env.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const test = require('supertape'); 5 | const env = require('./env'); 6 | 7 | test('cloudcmd: server: env: bool: upper case first', (t) => { 8 | const { 9 | CLOUDCMD_TERMINAL, 10 | cloudcmd_terminal, 11 | } = process.env; 12 | 13 | process.env.cloudcmd_terminal = 'true'; 14 | process.env.CLOUDCMD_TERMINAL = 'false'; 15 | 16 | const result = env.bool('terminal'); 17 | 18 | process.env.cloudcmd_terminal = cloudcmd_terminal; 19 | process.env.CLOUDCMD_TERMINAL = CLOUDCMD_TERMINAL; 20 | 21 | t.notOk(result); 22 | t.end(); 23 | }); 24 | 25 | test('cloudcmd: server: env: bool: snake_case', (t) => { 26 | process.env.cloudcmd_config_auth = 'true'; 27 | 28 | const result = env.bool('configAuth'); 29 | 30 | t.ok(result); 31 | t.end(); 32 | }); 33 | 34 | test('cloudcmd: server: env: bool: number', (t) => { 35 | const {cloudcmd_terminal} = process.env; 36 | 37 | process.env.CLOUDCMD_TERMINAL = '1'; 38 | 39 | const result = env.bool('terminal'); 40 | 41 | process.env.CLOUDCMD_TERMINAL = cloudcmd_terminal; 42 | 43 | t.ok(result); 44 | t.end(); 45 | }); 46 | 47 | test('cloudcmd: server: env: bool: number: 0', (t) => { 48 | const {cloudcmd_terminal} = process.env; 49 | 50 | process.env.cloudcmd_terminal = '0'; 51 | 52 | const result = env.bool('terminal'); 53 | 54 | process.env.cloudcmd_terminal = cloudcmd_terminal; 55 | 56 | t.notOk(result); 57 | t.end(); 58 | }); 59 | -------------------------------------------------------------------------------- /server/exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const getMessage = (a) => a?.message || a; 5 | 6 | module.exports = (...args) => { 7 | const messages = args.map(getMessage); 8 | 9 | console.error(...messages); 10 | process.exit(1); 11 | }; 12 | -------------------------------------------------------------------------------- /server/exit.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const {test, stub} = require('supertape'); 5 | 6 | const exit = require('./exit'); 7 | 8 | test('cloudcmd: exit: process.exit', (t) => { 9 | const {exit: exitOriginal} = process; 10 | 11 | process.exit = stub(); 12 | 13 | exit(); 14 | t.calledWith(process.exit, [1], 'should call process.exit'); 15 | process.exit = exitOriginal; 16 | 17 | t.end(); 18 | }); 19 | 20 | test('cloudcmd: exit: console.error', (t) => { 21 | const {exit: exitOriginal} = process; 22 | const {error} = console; 23 | 24 | console.error = stub(); 25 | process.exit = stub(); 26 | 27 | exit('hello world'); 28 | t.calledWith(console.error, ['hello world'], 'should call console.error'); 29 | 30 | process.exit = exitOriginal; 31 | console.error = error; 32 | 33 | t.end(); 34 | }); 35 | 36 | test('cloudcmd: exit.error: console.error: error', (t) => { 37 | const {exit: exitOriginal} = process; 38 | const {error} = console; 39 | 40 | console.error = stub(); 41 | process.exit = stub(); 42 | 43 | exit(Error('hello world')); 44 | t.calledWith(console.error, ['hello world'], 'should call console.error'); 45 | 46 | process.exit = exitOriginal; 47 | console.error = error; 48 | 49 | t.end(); 50 | }); 51 | -------------------------------------------------------------------------------- /server/fixture-user-menu/io-cp-fix.js: -------------------------------------------------------------------------------- 1 | async function copy() { 2 | await IO.copy(dirPath, mp3Dir, mp3Names); 3 | } 4 | -------------------------------------------------------------------------------- /server/fixture-user-menu/io-cp.js: -------------------------------------------------------------------------------- 1 | async function copy() { 2 | await IO.cp({ 3 | from: dirPath, 4 | to: mp3Dir, 5 | names: mp3Names, 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /server/fixture-user-menu/io-mv-fix.js: -------------------------------------------------------------------------------- 1 | async function move() { 2 | await IO.move(dirPath, mp3Dir, mp3Names); 3 | } 4 | -------------------------------------------------------------------------------- /server/fixture-user-menu/io-mv.js: -------------------------------------------------------------------------------- 1 | async function move() { 2 | await IO.mv({ 3 | from: dirPath, 4 | to: mp3Dir, 5 | names: mp3Names, 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /server/fixture/route.js: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /server/markdown/fixture/markdown.html: -------------------------------------------------------------------------------- 1 |

hello

2 | -------------------------------------------------------------------------------- /server/markdown/fixture/markdown.md: -------------------------------------------------------------------------------- 1 | # hello 2 | -------------------------------------------------------------------------------- /server/markdown/fixture/markdown.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/server/markdown/fixture/markdown.zip -------------------------------------------------------------------------------- /server/markdown/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {join} = require('node:path'); 4 | const {callbackify} = require('node:util'); 5 | 6 | const pullout = require('pullout'); 7 | const ponse = require('ponse'); 8 | const threadIt = require('thread-it'); 9 | const {read} = require('redzip'); 10 | 11 | const root = require('../root'); 12 | const isString = (a) => typeof a === 'string'; 13 | const parse = threadIt(join(__dirname, 'worker')); 14 | 15 | threadIt.init(); 16 | // warm up 17 | parse(''); 18 | 19 | const DIR_ROOT = `${__dirname}/../../`; 20 | 21 | module.exports = callbackify(async (name, rootDir, request) => { 22 | check(name, request); 23 | 24 | const {method} = request; 25 | 26 | switch(method) { 27 | case 'GET': 28 | return await onGET(request, name, rootDir); 29 | 30 | case 'PUT': 31 | return await onPUT(request); 32 | } 33 | }); 34 | 35 | function parseName(query, name, rootDir) { 36 | const shortName = name.replace('/markdown', ''); 37 | 38 | if (query === 'relative') 39 | return DIR_ROOT + shortName; 40 | 41 | return root(shortName, rootDir); 42 | } 43 | 44 | async function onGET(request, name, root) { 45 | const query = ponse.getQuery(request); 46 | const fileName = parseName(query, name, root); 47 | const stream = await read(fileName); 48 | const data = await pullout(stream); 49 | 50 | return parse(data); 51 | } 52 | 53 | async function onPUT(request) { 54 | const data = await pullout(request); 55 | return parse(data); 56 | } 57 | 58 | function check(name, request) { 59 | if (!isString(name)) 60 | throw Error('name should be string!'); 61 | 62 | if (!request) 63 | throw Error('request could not be empty!'); 64 | } 65 | -------------------------------------------------------------------------------- /server/markdown/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const markdownIt = require('markdown-it')(); 4 | 5 | module.exports = (a) => markdownIt.render(a); 6 | -------------------------------------------------------------------------------- /server/modulas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const deepmerge = require('deepmerge'); 4 | const originalModules = require('../json/modules'); 5 | 6 | module.exports = (modules) => { 7 | const result = deepmerge(originalModules, modules || {}); 8 | 9 | return (req, res, next) => { 10 | if (req.url !== '/json/modules.json') 11 | return next(); 12 | 13 | res.send(result); 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /server/prefixer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isString = (a) => typeof a === 'string'; 4 | 5 | module.exports = (value) => { 6 | if (!isString(value)) 7 | return ''; 8 | 9 | if (value.length === 1) 10 | return ''; 11 | 12 | if (value && !value.includes('/')) 13 | return `/${value}`; 14 | 15 | return value; 16 | }; 17 | -------------------------------------------------------------------------------- /server/prefixer.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | 5 | const prefixer = require('./prefixer'); 6 | 7 | test('prefixer: prefix without a slash', (t) => { 8 | t.equal(prefixer('hello'), '/hello', 'should add slash'); 9 | t.end(); 10 | }); 11 | 12 | test('prefixer: root', (t) => { 13 | t.equal(prefixer('/'), '', 'should add slash'); 14 | t.end(); 15 | }); 16 | 17 | test('prefixer: with slash', (t) => { 18 | t.equal(prefixer('/hello'), '/hello', 'should add slash'); 19 | t.end(); 20 | }); 21 | 22 | test('prefixer: not a string', (t) => { 23 | t.equal(prefixer(false), '', 'should add slash'); 24 | t.end(); 25 | }); 26 | -------------------------------------------------------------------------------- /server/repl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const net = require('node:net'); 5 | const repl = require('node:repl'); 6 | 7 | module.exports = net 8 | .createServer((socket) => { 9 | const {pid} = process; 10 | const addr = socket.remoteAddress; 11 | const port = socket.remotePort; 12 | 13 | const r = repl.start({ 14 | prompt: `[${pid} ${addr}:${port}>`, 15 | input: socket, 16 | output: socket, 17 | terminal: true, 18 | useGlobal: false, 19 | }); 20 | 21 | r.on('exit', () => { 22 | socket.end(); 23 | }); 24 | 25 | r.context.socket = socket; 26 | }) 27 | .listen(1337); 28 | -------------------------------------------------------------------------------- /server/rest/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const tryToCatch = require('try-to-catch'); 5 | 6 | const { 7 | _formatMsg, 8 | _getWin32RootMsg, 9 | _isRootWin32, 10 | _isRootAll, 11 | _onPUT, 12 | } = require('.'); 13 | 14 | test('rest: formatMsg', (t) => { 15 | const result = _formatMsg('hello', 'world'); 16 | 17 | t.equal(result, 'hello: ok("world")', 'should be equal'); 18 | t.end(); 19 | }); 20 | 21 | test('rest: formatMsg: json', (t) => { 22 | const result = _formatMsg('hello', { 23 | name: 'world', 24 | }); 25 | 26 | t.equal(result, 'hello: ok("{"name":"world"}")', 'should parse json'); 27 | t.end(); 28 | }); 29 | 30 | test('rest: getWin32RootMsg', (t) => { 31 | const {message} = _getWin32RootMsg(); 32 | 33 | t.equal(message, 'Could not copy from/to root on windows!', 'should return error'); 34 | t.end(); 35 | }); 36 | 37 | test('rest: isRootWin32', (t) => { 38 | const result = _isRootWin32('/', '/'); 39 | 40 | t.notOk(result, 'should equal'); 41 | t.end(); 42 | }); 43 | 44 | test('rest: isRootAll', (t) => { 45 | const result = _isRootAll('/', ['/', '/h']); 46 | 47 | t.notOk(result, 'should equal'); 48 | t.end(); 49 | }); 50 | 51 | test('rest: onPUT: no args', async (t) => { 52 | const [e] = await tryToCatch(_onPUT, {}); 53 | 54 | t.equal(e.message, 'name should be a string!', 'should throw when no args'); 55 | t.end(); 56 | }); 57 | 58 | test('rest: onPUT: no body', async (t) => { 59 | const [e] = await tryToCatch(_onPUT, { 60 | name: 'hello', 61 | }); 62 | 63 | t.equal(e.message, 'body should be a string!', 'should throw when no body'); 64 | t.end(); 65 | }); 66 | 67 | test('rest: onPUT: no callback', async (t) => { 68 | const [e] = await tryToCatch(_onPUT, { 69 | name: 'hello', 70 | body: 'world', 71 | }); 72 | 73 | t.equal(e.message, 'callback should be a function!', 'should throw when no callback'); 74 | t.end(); 75 | }); 76 | -------------------------------------------------------------------------------- /server/rest/info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const format = require('format-io'); 5 | 6 | const {version} = require('../../package'); 7 | 8 | const getMemory = () => { 9 | const {rss} = process.memoryUsage(); 10 | return format.size(rss); 11 | }; 12 | 13 | module.exports = (prefix) => ({ 14 | version, 15 | prefix, 16 | memory: getMemory(), 17 | }); 18 | -------------------------------------------------------------------------------- /server/rest/info.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const {test, stub} = require('supertape'); 5 | 6 | const info = require('./info'); 7 | 8 | test('cloudcmd: rest: info', (t) => { 9 | const {memoryUsage} = process; 10 | 11 | const _memoryUsage = stub().returns({}); 12 | 13 | process.memoryUsage = _memoryUsage; 14 | 15 | info(); 16 | 17 | process.memoryUsage = memoryUsage; 18 | 19 | t.calledWithNoArgs(_memoryUsage, 'should call memoryUsage'); 20 | t.end(); 21 | }); 22 | -------------------------------------------------------------------------------- /server/root.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mellow = require('mellow'); 4 | 5 | module.exports = (dir, root, {webToWin = mellow.webToWin} = {}) => { 6 | return webToWin(dir, root || '/'); 7 | }; 8 | -------------------------------------------------------------------------------- /server/root.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const root = require('./root'); 6 | 7 | test('cloudcmd: root: mellow', (t) => { 8 | const webToWin = stub(); 9 | 10 | const dir = 'hello'; 11 | const dirRoot = '/'; 12 | 13 | root(dir, '', { 14 | webToWin, 15 | }); 16 | 17 | t.calledWith(webToWin, [dir, dirRoot], 'should call mellow'); 18 | t.end(); 19 | }); 20 | -------------------------------------------------------------------------------- /server/server.mjs: -------------------------------------------------------------------------------- 1 | import http from 'node:http'; 2 | import {promisify} from 'node:util'; 3 | import process from 'node:process'; 4 | import currify from 'currify'; 5 | import squad from 'squad'; 6 | import tryToCatch from 'try-to-catch'; 7 | import opn from 'open'; 8 | import express from 'express'; 9 | import {Server} from 'socket.io'; 10 | import tryRequire from 'tryrequire'; 11 | import wraptile from 'wraptile'; 12 | import compression from 'compression'; 13 | import threadIt from 'thread-it'; 14 | import exit from './exit.js'; 15 | import cloudcmd from './cloudcmd.mjs'; 16 | 17 | const bind = (f, self) => f.bind(self); 18 | 19 | const two = currify((f, a, b) => f(a, b)); 20 | const shutdown = wraptile(async (promises) => { 21 | console.log('closing cloudcmd...'); 22 | await Promise.all(promises); 23 | threadIt.terminate(); 24 | process.exit(0); 25 | }); 26 | 27 | const promisifySelf = squad(promisify, bind); 28 | 29 | const exitPort = two(exit, 'cloudcmd --port: %s'); 30 | const logger = tryRequire('morgan'); 31 | 32 | export default async (options, config) => { 33 | const prefix = config('prefix'); 34 | const port = process.env.PORT /* c9 */ || config('port'); 35 | 36 | const ip = process.env.IP /* c9 */ || config('ip') || '0.0.0.0'; 37 | 38 | const app = express(); 39 | const server = http.createServer(app); 40 | 41 | if (logger) 42 | app.use(logger('dev')); 43 | 44 | if (prefix) 45 | app.get('/', (req, res) => res.redirect(`${prefix}/`)); 46 | 47 | const socketServer = new Server(server, { 48 | path: `${prefix}/socket.io`, 49 | }); 50 | 51 | app.use(compression()); 52 | 53 | app.use(prefix, cloudcmd({ 54 | config: options, 55 | socket: socketServer, 56 | configManager: config, 57 | })); 58 | 59 | if (port < 0 || port > 65_535) 60 | return exitPort('port number could be 1..65535, 0 means any available port'); 61 | 62 | const listen = promisifySelf(server.listen, server); 63 | const closeServer = promisifySelf(server.close, server); 64 | const closeSocket = promisifySelf(socketServer.close, socketServer); 65 | 66 | server.on('error', exitPort); 67 | await listen(port, ip); 68 | 69 | const close = shutdown([closeServer, closeSocket]); 70 | process.on('SIGINT', close); 71 | 72 | const host = config('ip') || 'localhost'; 73 | const port0 = port || server.address().port; 74 | const url = `http://${host}:${port0}${prefix}/`; 75 | 76 | console.log(`url: ${url}`); 77 | 78 | if (!config('open')) 79 | return; 80 | 81 | const [openError] = await tryToCatch(opn, url); 82 | 83 | if (openError) 84 | console.error('cloudcmd --open:', openError.message); 85 | }; 86 | -------------------------------------------------------------------------------- /server/show-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | table, 5 | getBorderCharacters, 6 | } = require('table'); 7 | 8 | module.exports = (config) => { 9 | check(config); 10 | 11 | const data = Object 12 | .keys(config) 13 | .map((name) => [ 14 | name, 15 | config[name], 16 | ]); 17 | 18 | if (!data.length) 19 | return ''; 20 | 21 | return table(data, { 22 | columns: { 23 | 1: { 24 | width: 30, 25 | truncate: 30, 26 | }, 27 | }, 28 | border: getBorderCharacters('ramac'), 29 | }); 30 | }; 31 | 32 | function check(config) { 33 | if (!config) 34 | throw Error('config could not be empty!'); 35 | 36 | if (typeof config !== 'object') 37 | throw Error('config should be an object!'); 38 | } 39 | -------------------------------------------------------------------------------- /server/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('node:path'); 4 | const readFilesSync = require('@cloudcmd/read-files-sync'); 5 | const templatePath = path.join(__dirname, '..', 'tmpl/fs'); 6 | 7 | module.exports = readFilesSync(templatePath, 'utf8'); 8 | -------------------------------------------------------------------------------- /server/terminal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tryCatch = require('try-catch'); 4 | 5 | const noop = (req, res, next) => { 6 | next && next(); 7 | }; 8 | 9 | noop.listen = noop; 10 | 11 | const _getModule = (a) => require(a); 12 | 13 | module.exports = (config, arg, overrides = {}) => { 14 | const { 15 | getModule = _getModule, 16 | } = overrides; 17 | 18 | if (!config('terminal')) 19 | return noop; 20 | 21 | const [e, terminalModule] = tryCatch(getModule, config('terminalPath')); 22 | 23 | if (!e && !arg) 24 | return terminalModule; 25 | 26 | if (!e) 27 | return terminalModule(arg); 28 | 29 | config('terminal', false); 30 | console.log(`cloudcmd --terminal: ${e.message}`); 31 | 32 | return noop; 33 | }; 34 | -------------------------------------------------------------------------------- /server/terminal.spec.mjs: -------------------------------------------------------------------------------- 1 | import {test, stub} from 'supertape'; 2 | import terminal from './terminal.js'; 3 | import {createConfigManager} from './cloudcmd.mjs'; 4 | 5 | test('cloudcmd: terminal: disabled', (t) => { 6 | const config = createConfigManager(); 7 | config('terminal', false); 8 | 9 | const fn = terminal(config); 10 | 11 | t.notOk(fn(), 'should return noop'); 12 | t.end(); 13 | }); 14 | 15 | test('cloudcmd: terminal: disabled: listen', (t) => { 16 | const config = createConfigManager(); 17 | config('terminal', false); 18 | 19 | const fn = terminal(config).listen(); 20 | 21 | t.notOk(fn, 'should return noop'); 22 | t.end(); 23 | }); 24 | 25 | test('cloudcmd: terminal: enabled', (t) => { 26 | const term = stub(); 27 | const arg = 'hello'; 28 | const config = stub().returns(true); 29 | const getModule = stub().returns(term); 30 | 31 | terminal(config, arg, { 32 | getModule, 33 | }); 34 | 35 | t.calledWith(term, [arg], 'should call terminal'); 36 | t.end(); 37 | }); 38 | 39 | test('cloudcmd: terminal: enabled: no string', (t) => { 40 | const {log: originalLog} = console; 41 | const log = stub(); 42 | 43 | console.log = log; 44 | const config = createConfigManager(); 45 | 46 | config('terminal', true); 47 | config('terminalPath', 'hello'); 48 | terminal(config); 49 | 50 | console.log = originalLog; 51 | 52 | const msg = `cloudcmd --terminal: Cannot find module 'hello'`; 53 | const [arg] = log.args[0]; 54 | 55 | t.match(arg, RegExp(msg), 'should call with msg'); 56 | t.end(); 57 | }); 58 | 59 | test('cloudcmd: terminal: no arg', (t) => { 60 | const gritty = {}; 61 | const getModule = stub().returns(gritty); 62 | const config = createConfigManager(); 63 | 64 | config('terminal', true); 65 | config('terminalPath', 'gritty'); 66 | 67 | const result = terminal(config, '', { 68 | getModule, 69 | }); 70 | 71 | t.equal(result, gritty); 72 | t.end(); 73 | }); 74 | -------------------------------------------------------------------------------- /server/theme.mjs: -------------------------------------------------------------------------------- 1 | import path, {dirname} from 'node:path'; 2 | import {fileURLToPath} from 'node:url'; 3 | import process from 'node:process'; 4 | import fs from 'node:fs'; 5 | import fullstore from 'fullstore'; 6 | import nanomemoizeDefault from 'nano-memoize'; 7 | import readFilesSync from '@cloudcmd/read-files-sync'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | const isMap = (a) => /\.map$/.test(a); 12 | const not = (fn) => (a) => !fn(a); 13 | 14 | const _isDev = fullstore(process.env.NODE_ENV === 'development'); 15 | const getDist = (isDev) => isDev ? 'dist-dev' : 'dist'; 16 | 17 | export const isDev = _isDev; 18 | 19 | export const getThemes = ({isDev = _isDev()} = {}) => { 20 | return readFilesSyncMemo(isDev); 21 | }; 22 | 23 | const {nanomemoize} = nanomemoizeDefault; 24 | 25 | const readFilesSyncMemo = nanomemoize((isDev) => { 26 | const dist = getDist(isDev); 27 | const themesDir = path.join(__dirname, '..', dist, 'themes'); 28 | const names = fs 29 | .readdirSync(themesDir) 30 | .filter(not(isMap)); 31 | 32 | return readFilesSync(themesDir, names, 'utf8'); 33 | }); 34 | -------------------------------------------------------------------------------- /server/themes.spec.mjs: -------------------------------------------------------------------------------- 1 | import {dirname} from 'node:path'; 2 | import {fileURLToPath} from 'node:url'; 3 | import fs from 'node:fs'; 4 | import test from 'supertape'; 5 | import {getThemes, isDev} from './theme.mjs'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = dirname(__filename); 9 | 10 | test('themes: dev', (t) => { 11 | const themes = getThemes({ 12 | isDev: true, 13 | }); 14 | 15 | const css = fs.readFileSync(`${__dirname}/../css/themes/dark.css`, 'utf8'); 16 | 17 | t.equal(themes.dark, css); 18 | t.end(); 19 | }); 20 | 21 | test('themes: no args', (t) => { 22 | const currentIsDev = isDev(); 23 | isDev(true); 24 | const themes = getThemes(); 25 | 26 | const css = fs.readFileSync(`${__dirname}/../css/themes/light.css`, 'utf8'); 27 | isDev(currentIsDev); 28 | 29 | t.equal(themes.light, css); 30 | t.end(); 31 | }); 32 | -------------------------------------------------------------------------------- /server/user-menu.spec.mjs: -------------------------------------------------------------------------------- 1 | import {dirname, join} from 'node:path'; 2 | import {fileURLToPath} from 'node:url'; 3 | import {readFileSync} from 'node:fs'; 4 | import {test, stub} from 'supertape'; 5 | import serveOnce from 'serve-once'; 6 | import threadIt from 'thread-it'; 7 | import userMenu from './user-menu.mjs'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | const {request} = serveOnce(userMenu); 13 | const userMenuPath = join(__dirname, '..', '.cloudcmd.menu.js'); 14 | const userMenuFile = readFileSync(userMenuPath, 'utf8'); 15 | 16 | const fixtureDir = new URL('fixture-user-menu', import.meta.url).pathname; 17 | const fixtureMoveName = join(fixtureDir, 'io-mv.js'); 18 | const fixtureMoveFixName = join(fixtureDir, 'io-mv-fix.js'); 19 | const fixtureCopyName = join(fixtureDir, 'io-cp.js'); 20 | const fixtureCopyFixName = join(fixtureDir, 'io-cp-fix.js'); 21 | 22 | const fixtureMove = readFileSync(fixtureMoveName, 'utf8'); 23 | const fixtureMoveFix = readFileSync(fixtureMoveFixName, 'utf8'); 24 | const fixtureCopy = readFileSync(fixtureCopyName, 'utf8'); 25 | const fixtureCopyFix = readFileSync(fixtureCopyFixName, 'utf8'); 26 | 27 | test('cloudcmd: user menu', async (t) => { 28 | const options = { 29 | menuName: '.cloudcmd.menu.js', 30 | }; 31 | 32 | const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`, { 33 | options, 34 | }); 35 | 36 | t.equal(userMenuFile, body); 37 | t.end(); 38 | }); 39 | 40 | test('cloudcmd: user menu: io.mv', async (t) => { 41 | const readFile = stub().returns(fixtureMove); 42 | const options = { 43 | menuName: '.cloudcmd.menu.js', 44 | readFile, 45 | }; 46 | 47 | const {request} = serveOnce(userMenu); 48 | 49 | const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`, { 50 | options, 51 | }); 52 | 53 | t.equal(body, fixtureMoveFix); 54 | t.end(); 55 | }); 56 | 57 | test('cloudcmd: user menu: io.cp', async (t) => { 58 | const readFile = stub().returns(fixtureCopy); 59 | const options = { 60 | menuName: '.cloudcmd.menu.js', 61 | readFile, 62 | }; 63 | 64 | const {request} = serveOnce(userMenu); 65 | 66 | const {body} = await request.get(`/api/v1/user-menu?dir=${__dirname}`, { 67 | options, 68 | }); 69 | 70 | threadIt.terminate(); 71 | 72 | t.equal(body, fixtureCopyFix); 73 | t.end(); 74 | }); 75 | -------------------------------------------------------------------------------- /server/validate.mjs: -------------------------------------------------------------------------------- 1 | import {statSync as _statSync} from 'node:fs'; 2 | import tryCatch from 'try-catch'; 3 | import _exit from './exit.js'; 4 | import {getColumns as _getColumns} from './columns.mjs'; 5 | import {getThemes as _getThemes} from './theme.mjs'; 6 | 7 | const isString = (a) => typeof a === 'string'; 8 | 9 | export const root = (dir, config, overrides = {}) => { 10 | const { 11 | exit = _exit, 12 | statSync = _statSync, 13 | } = overrides; 14 | 15 | if (!isString(dir)) 16 | throw Error('dir should be a string'); 17 | 18 | if (dir === '/') 19 | return; 20 | 21 | if (config('dropbox')) 22 | return; 23 | 24 | const [error] = tryCatch(statSync, dir); 25 | 26 | if (error) 27 | return exit('cloudcmd --root: %s', error.message); 28 | }; 29 | 30 | export const editor = (name, {exit = _exit} = {}) => { 31 | const reg = /^(dword|edward|deepword)$/; 32 | 33 | if (!reg.test(name)) 34 | exit('cloudcmd --editor: could be "dword", "edward" or "deepword" only'); 35 | }; 36 | 37 | export const packer = (name, {exit = _exit} = {}) => { 38 | const reg = /^(tar|zip)$/; 39 | 40 | if (!reg.test(name)) 41 | exit('cloudcmd --packer: could be "tar" or "zip" only'); 42 | }; 43 | 44 | export const columns = (type, overrides = {}) => { 45 | const { 46 | exit = _exit, 47 | getColumns = _getColumns, 48 | } = overrides; 49 | 50 | const addQuotes = (a) => `"${a}"`; 51 | const all = Object 52 | .keys(getColumns()) 53 | .concat(''); 54 | 55 | const names = all 56 | .filter(Boolean) 57 | .map(addQuotes) 58 | .join(', '); 59 | 60 | if (!all.includes(type)) 61 | exit(`cloudcmd --columns: can be only one of: ${names}`); 62 | }; 63 | 64 | export const theme = (type, overrides = {}) => { 65 | const { 66 | exit = _exit, 67 | getThemes = _getThemes, 68 | } = overrides; 69 | 70 | const addQuotes = (a) => `"${a}"`; 71 | const all = Object 72 | .keys(getThemes()) 73 | .concat(''); 74 | 75 | const names = all 76 | .filter(Boolean) 77 | .map(addQuotes) 78 | .join(', '); 79 | 80 | if (!all.includes(type)) 81 | exit(`cloudcmd --theme: can be only one of: ${names}`); 82 | }; 83 | -------------------------------------------------------------------------------- /test/before.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import http from 'node:http'; 3 | import os from 'node:os'; 4 | import {promisify} from 'node:util'; 5 | import {fileURLToPath} from 'node:url'; 6 | import {dirname} from 'node:path'; 7 | import express from 'express'; 8 | import {Server} from 'socket.io'; 9 | import writejson from 'writejson'; 10 | import readjson from 'readjson'; 11 | import cloudcmd from '../server/cloudcmd.mjs'; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = dirname(__filename); 15 | 16 | process.env.NODE_ENV = 'development'; 17 | const {assign} = Object; 18 | 19 | const pathConfig = os.homedir() + '/.cloudcmd.json'; 20 | const currentConfig = readjson.sync.try(pathConfig); 21 | 22 | export default before; 23 | 24 | function before(options, fn = options) { 25 | const { 26 | config, 27 | plugins, 28 | modules, 29 | configManager, 30 | } = options; 31 | 32 | const app = express(); 33 | const server = http.createServer(app); 34 | 35 | const after = (cb) => { 36 | if (currentConfig) 37 | writejson.sync(pathConfig, currentConfig); 38 | 39 | server.close(cb); 40 | }; 41 | 42 | const socket = new Server(server); 43 | 44 | app.use(cloudcmd({ 45 | socket, 46 | plugins, 47 | config: assign(defaultConfig(), config), 48 | configManager, 49 | modules, 50 | })); 51 | 52 | server.listen(() => { 53 | fn(server 54 | .address().port, promisify(after)); 55 | }); 56 | } 57 | 58 | export const connect = promisify((options, fn = options) => { 59 | before(options, (port, done) => { 60 | fn(null, { 61 | port, 62 | done, 63 | }); 64 | }); 65 | }); 66 | 67 | const defaultConfig = () => ({ 68 | auth: false, 69 | root: __dirname, 70 | }); 71 | -------------------------------------------------------------------------------- /test/client/listeners/get-index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | 5 | const dir = '../../../client/listeners'; 6 | const getIndex = require(`${dir}/get-index`); 7 | 8 | test('cloudcmd: client: listeners: getIndex: not found', (t) => { 9 | const array = ['hello']; 10 | 11 | t.equal(getIndex(array, 'world'), 0, 'should return index'); 12 | t.end(); 13 | }); 14 | 15 | test('cloudcmd: client: listeners: getIndex: found', (t) => { 16 | const array = [ 17 | 'hello', 18 | 'world', 19 | ]; 20 | 21 | t.equal(getIndex(array, 'world'), 1, 'should return index'); 22 | t.end(); 23 | }); 24 | -------------------------------------------------------------------------------- /test/client/listeners/get-range.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | 5 | const dir = '../../../client/listeners'; 6 | const getRange = require(`${dir}/get-range`); 7 | 8 | test('cloudcmd: client: listeners: getRange: direct', (t) => { 9 | const expected = [ 10 | 'hello', 11 | 'world', 12 | ]; 13 | 14 | const files = [ 15 | ...expected, 16 | 'how', 17 | 'come', 18 | ]; 19 | 20 | const result = getRange(0, 1, files); 21 | 22 | t.deepEqual(result, expected, 'should return range'); 23 | t.end(); 24 | }); 25 | 26 | test('cloudcmd: client: listeners: getRange: reverse', (t) => { 27 | const expected = [ 28 | 'hello', 29 | 'world', 30 | ]; 31 | 32 | const files = [ 33 | ...expected, 34 | 'how', 35 | 'come', 36 | ]; 37 | 38 | const result = getRange(1, 0, files); 39 | 40 | t.deepEqual(result, expected, 'should return range'); 41 | t.end(); 42 | }); 43 | 44 | test('cloudcmd: client: listeners: getRange: one', (t) => { 45 | const expected = ['hello']; 46 | const files = [ 47 | ...expected, 48 | 'how', 49 | 'come', 50 | ]; 51 | 52 | const result = getRange(0, 0, files); 53 | 54 | t.deepEqual(result, expected, 'should return range'); 55 | t.end(); 56 | }); 57 | -------------------------------------------------------------------------------- /test/common/cloudfunc.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | name 4 | size 5 | date 6 | owner 7 | mode 8 |
  • 9 | 10 | .. 11 | <dir> 12 | --.--.---- 13 | . 14 | --- --- --- 15 |
  • 16 | 17 | applnk 18 | <dir> 19 | 21.02.2016 20 | root 21 | rwx r-x r-x 22 |
  • 23 | 24 | ай 25 | 1.30kb 26 | --.--.---- 27 | root 28 | rwx r-x r-x 29 |
-------------------------------------------------------------------------------- /test/fixture/copy.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /test/fixture/empty-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/test/fixture/empty-file -------------------------------------------------------------------------------- /test/fixture/pack: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /test/fixture/pack.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/test/fixture/pack.tar.gz -------------------------------------------------------------------------------- /test/fixture/pack.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderaiser/cloudcmd/b26c8bba3744a72e756df76e4264a434b16dacd6/test/fixture/pack.zip -------------------------------------------------------------------------------- /test/rest/config.mjs: -------------------------------------------------------------------------------- 1 | import serveOnce from 'serve-once'; 2 | import test from 'supertape'; 3 | import cloudcmd from '../../server/cloudcmd.mjs'; 4 | 5 | const configManager = cloudcmd.createConfigManager(); 6 | const {request} = serveOnce(cloudcmd, { 7 | config: { 8 | auth: false, 9 | }, 10 | configManager, 11 | }); 12 | 13 | test('cloudcmd: rest: config: get', async (t) => { 14 | const {body} = await request.get('/api/v1/config', { 15 | type: 'json', 16 | }); 17 | 18 | t.notOk(body.auth, 'should config.auth to be false'); 19 | t.end(); 20 | }); 21 | 22 | test('cloudcmd: rest: config: patch', async (t) => { 23 | const configDialog = true; 24 | const config = { 25 | configDialog, 26 | }; 27 | 28 | const options = { 29 | config, 30 | }; 31 | 32 | const body = { 33 | auth: false, 34 | }; 35 | 36 | const res = await request.patch('/api/v1/config', { 37 | options, 38 | body, 39 | }); 40 | 41 | const result = res.body; 42 | 43 | t.equal(result, 'config: ok("auth")', 'should patch config'); 44 | t.end(); 45 | }); 46 | 47 | test('cloudcmd: rest: config: patch: no configDialog', async (t) => { 48 | const config = { 49 | configDialog: false, 50 | }; 51 | 52 | const options = { 53 | config, 54 | }; 55 | 56 | const body = { 57 | ip: null, 58 | }; 59 | 60 | const result = await request.patch(`/api/v1/config`, { 61 | body, 62 | options, 63 | }); 64 | 65 | t.equal(result.body, 'Config is disabled', 'should return error'); 66 | t.end(); 67 | }); 68 | 69 | test('cloudcmd: rest: config: patch: no configDialog: statusCode', async (t) => { 70 | const config = { 71 | configDialog: false, 72 | }; 73 | 74 | const options = { 75 | config, 76 | }; 77 | 78 | const body = { 79 | ip: null, 80 | }; 81 | 82 | const response = await request.patch(`/api/v1/config`, { 83 | body, 84 | options, 85 | }); 86 | 87 | configManager('configDialog', true); 88 | 89 | t.equal(response.status, 404); 90 | t.end(); 91 | }); 92 | 93 | test('cloudcmd: rest: config: patch: save config', async (t) => { 94 | const body = { 95 | editor: 'dword', 96 | }; 97 | 98 | await request.patch(`/api/v1/config`, { 99 | body, 100 | }); 101 | 102 | t.equal(configManager('editor'), 'dword', 'should change config file on patch'); 103 | t.end(); 104 | }); 105 | -------------------------------------------------------------------------------- /test/rest/copy.mjs: -------------------------------------------------------------------------------- 1 | import {dirname, join} from 'node:path'; 2 | import {fileURLToPath} from 'node:url'; 3 | import {mkdirSync} from 'node:fs'; 4 | import serveOnce from 'serve-once'; 5 | import test from 'supertape'; 6 | import {rimraf} from 'rimraf'; 7 | import cloudcmd from '../../server/cloudcmd.mjs'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | const config = { 13 | root: new URL('..', import.meta.url).pathname, 14 | }; 15 | 16 | const configManager = cloudcmd.createConfigManager(); 17 | 18 | configManager('auth', false); 19 | const {request} = serveOnce(cloudcmd, { 20 | config, 21 | configManager, 22 | }); 23 | 24 | const fixtureDir = join(__dirname, '..', 'fixture') + '/'; 25 | 26 | test('cloudcmd: rest: copy', async (t) => { 27 | const tmp = join(fixtureDir, 'tmp'); 28 | const files = { 29 | from: '/fixture/', 30 | to: '/fixture/tmp', 31 | names: ['copy.txt'], 32 | }; 33 | 34 | mkdirSync(tmp); 35 | 36 | const {body} = await request.put(`/api/v1/copy`, { 37 | body: files, 38 | }); 39 | 40 | rimraf.sync(tmp); 41 | 42 | t.equal(body, 'copy: ok("["copy.txt"]")', 'should return result'); 43 | t.end(); 44 | }); 45 | -------------------------------------------------------------------------------- /test/rest/fs.mjs: -------------------------------------------------------------------------------- 1 | import serveOnce from 'serve-once'; 2 | import test from 'supertape'; 3 | import cloudcmd from '../../server/cloudcmd.mjs'; 4 | 5 | const {request} = serveOnce(cloudcmd, { 6 | config: { 7 | auth: false, 8 | }, 9 | }); 10 | 11 | test('cloudcmd: rest: fs: path', async (t) => { 12 | const {body} = await request.get(`/api/v1/fs`, { 13 | type: 'json', 14 | }); 15 | 16 | const {path} = body; 17 | 18 | t.equal(path, '/', 'should dir path be "/"'); 19 | t.end(); 20 | }); 21 | 22 | test('cloudcmd: path traversal beyond root', async (t) => { 23 | const {body} = await request.get('/fs..%2f..%2fetc/passwd'); 24 | 25 | t.match(body, 'beyond root', 'should return beyond root message'); 26 | t.end(); 27 | }); 28 | -------------------------------------------------------------------------------- /test/rest/move.mjs: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'node:events'; 2 | import wait from '@iocmd/wait'; 3 | import {test, stub} from 'supertape'; 4 | import serveOnce from 'serve-once'; 5 | import cloudcmd from '../../server/cloudcmd.mjs'; 6 | 7 | test('cloudcmd: rest: move', async (t) => { 8 | const move = new EventEmitter(); 9 | const moveFiles = stub().returns(move); 10 | 11 | const {createConfigManager} = cloudcmd; 12 | cloudcmd.depStore('moveFiles', moveFiles); 13 | 14 | const configManager = createConfigManager(); 15 | configManager('auth', false); 16 | configManager('root', '/'); 17 | 18 | const {request} = serveOnce(cloudcmd, { 19 | configManager, 20 | }); 21 | 22 | const files = { 23 | from: '/fixture/', 24 | to: '/fixture/tmp/', 25 | names: ['move.txt'], 26 | }; 27 | 28 | const emit = move.emit.bind(move); 29 | 30 | const [{body}] = await Promise.all([ 31 | request.put(`/api/v1/move`, { 32 | body: files, 33 | }), 34 | wait(1000, emit, 'end'), 35 | ]); 36 | 37 | t.equal(body, 'move: ok("["move.txt"]")', 'should move'); 38 | t.end(); 39 | }); 40 | 41 | test('cloudcmd: rest: move: no from', async (t) => { 42 | const {createConfigManager} = cloudcmd; 43 | 44 | const configManager = createConfigManager(); 45 | configManager('auth', false); 46 | configManager('root', '/'); 47 | 48 | const {request} = serveOnce(cloudcmd, { 49 | configManager, 50 | }); 51 | 52 | const files = {}; 53 | 54 | const {body} = await request.put(`/api/v1/move`, { 55 | body: files, 56 | }); 57 | 58 | const expected = '"from" should be filled'; 59 | 60 | t.equal(body, expected); 61 | t.end(); 62 | }); 63 | 64 | test('cloudcmd: rest: move: no to', async (t) => { 65 | const {createConfigManager} = cloudcmd; 66 | 67 | const configManager = createConfigManager(); 68 | configManager('auth', false); 69 | configManager('root', '/'); 70 | 71 | const {request} = serveOnce(cloudcmd, { 72 | configManager, 73 | }); 74 | 75 | const files = { 76 | from: '/', 77 | }; 78 | 79 | const {body} = await request.put(`/api/v1/move`, { 80 | body: files, 81 | }); 82 | 83 | const expected = '"to" should be filled'; 84 | 85 | t.equal(body, expected); 86 | t.end(); 87 | }); 88 | -------------------------------------------------------------------------------- /test/rest/rename.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import test from 'supertape'; 3 | import {Volume} from 'memfs'; 4 | import {ufs} from 'unionfs'; 5 | import serveOnce from 'serve-once'; 6 | import cloudcmd from '../../server/cloudcmd.mjs'; 7 | 8 | test('cloudcmd: rest: rename', async (t) => { 9 | const volume = { 10 | '/fixture/mv.txt': 'hello', 11 | '/fixture/tmp/a.txt': 'a', 12 | }; 13 | 14 | const vol = Volume.fromJSON(volume, '/'); 15 | 16 | const unionFS = ufs 17 | .use(vol) 18 | .use(fs); 19 | 20 | const {createConfigManager} = cloudcmd; 21 | const configManager = createConfigManager(); 22 | 23 | configManager('auth', false); 24 | configManager('root', '/'); 25 | 26 | cloudcmd.depStore('fs', unionFS); 27 | const {request} = serveOnce(cloudcmd, { 28 | configManager, 29 | }); 30 | 31 | const files = { 32 | from: '/fixture/mv.txt', 33 | to: '/fixture/tmp/mv.txt', 34 | }; 35 | 36 | const {body} = await request.put(`/api/v1/rename`, { 37 | body: files, 38 | }); 39 | 40 | cloudcmd.depStore(); 41 | 42 | const expected = 'rename: ok("{"from":"/fixture/mv.txt","to":"/fixture/tmp/mv.txt"}")'; 43 | 44 | t.equal(body, expected, 'should move'); 45 | t.end(); 46 | }); 47 | 48 | test('cloudcmd: rest: rename: no from', async (t) => { 49 | const {createConfigManager} = cloudcmd; 50 | 51 | const configManager = createConfigManager(); 52 | configManager('auth', false); 53 | configManager('root', '/'); 54 | 55 | const {request} = serveOnce(cloudcmd, { 56 | configManager, 57 | }); 58 | 59 | const files = {}; 60 | 61 | const {body} = await request.put(`/api/v1/rename`, { 62 | body: files, 63 | }); 64 | 65 | const expected = '"from" should be filled'; 66 | 67 | t.equal(body, expected); 68 | t.end(); 69 | }); 70 | 71 | test('cloudcmd: rest: rename: no to', async (t) => { 72 | const {createConfigManager} = cloudcmd; 73 | 74 | const configManager = createConfigManager(); 75 | configManager('auth', false); 76 | configManager('root', '/'); 77 | 78 | const {request} = serveOnce(cloudcmd, { 79 | configManager, 80 | }); 81 | 82 | const files = { 83 | from: '/', 84 | }; 85 | 86 | const {body} = await request.put(`/api/v1/rename`, { 87 | body: files, 88 | }); 89 | 90 | const expected = '"to" should be filled'; 91 | 92 | t.equal(body, expected); 93 | t.end(); 94 | }); 95 | -------------------------------------------------------------------------------- /test/server/console.mjs: -------------------------------------------------------------------------------- 1 | import path, {dirname} from 'node:path'; 2 | import {once} from 'node:events'; 3 | import {fileURLToPath} from 'node:url'; 4 | import {createRequire} from 'node:module'; 5 | import test from 'supertape'; 6 | import io from 'socket.io-client'; 7 | import {connect} from '../before.mjs'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | const require = createRequire(import.meta.url); 12 | const configPath = path.join(__dirname, '../..', 'server', 'config'); 13 | const configFn = require(configPath).createConfig(); 14 | 15 | test('cloudcmd: console: enabled', async (t) => { 16 | const config = { 17 | console: true, 18 | }; 19 | 20 | const {port, done} = await connect({ 21 | config, 22 | }); 23 | 24 | const socket = io(`http://localhost:${port}/console`); 25 | 26 | socket.emit('auth', configFn('username'), configFn('password')); 27 | 28 | const [data] = await once(socket, 'data'); 29 | 30 | socket.close(); 31 | await done(); 32 | 33 | t.equal(data, 'client #1 console connected\n', 'should emit data event'); 34 | t.end(); 35 | }); 36 | 37 | test('cloudcmd: console: disabled', async (t) => { 38 | const config = { 39 | console: false, 40 | }; 41 | 42 | const {port, done} = await connect({ 43 | config, 44 | }); 45 | 46 | const socket = io(`http://localhost:${port}/console`); 47 | 48 | const [error] = await once(socket, 'connect_error'); 49 | 50 | socket.close(); 51 | await done(); 52 | 53 | t.equal(error.message, 'Invalid namespace', 'should emit error'); 54 | t.end(); 55 | }); 56 | -------------------------------------------------------------------------------- /test/server/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const test = require('supertape'); 5 | const env = require('../../server/env'); 6 | 7 | test('env: small', (t) => { 8 | process.env.cloudcmd_hello = 'world'; 9 | t.equal(env('hello'), 'world', 'should parse string from env'); 10 | 11 | delete process.env.cloudcmd_hello; 12 | t.end(); 13 | }); 14 | 15 | test('env: big', (t) => { 16 | process.env.CLOUDCMD_HELLO = 'world'; 17 | t.equal(env('hello'), 'world', 'should parse string from env'); 18 | 19 | delete process.env.CLOUDCMD_HELLO; 20 | t.end(); 21 | }); 22 | 23 | test('env: bool: false', (t) => { 24 | process.env.cloudcmd_terminal = 'false'; 25 | t.notOk(env.bool('terminal'), 'should return false'); 26 | 27 | delete process.env.cloudcmd_terminal; 28 | t.end(); 29 | }); 30 | 31 | test('env: bool: true', (t) => { 32 | process.env.cloudcmd_terminal = 'true'; 33 | 34 | t.ok(env.bool('terminal'), 'should be true'); 35 | 36 | delete process.env.cloudcmd_terminal; 37 | t.end(); 38 | }); 39 | 40 | test('env: bool: undefined', (t) => { 41 | const {cloudcmd_terminal} = process.env; 42 | 43 | process.env.cloudcmd_terminal = undefined; 44 | 45 | t.notOk(env.bool('terminal'), 'should be undefined'); 46 | 47 | process.env.cloudcmd_terminal = cloudcmd_terminal; 48 | t.end(); 49 | }); 50 | -------------------------------------------------------------------------------- /test/server/modulas.mjs: -------------------------------------------------------------------------------- 1 | import {createRequire} from 'node:module'; 2 | import {dirname, join} from 'node:path'; 3 | import {fileURLToPath} from 'node:url'; 4 | import serveOnce from 'serve-once'; 5 | import {test, stub} from 'supertape'; 6 | import cloudcmd from '../../server/cloudcmd.mjs'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | const require = createRequire(import.meta.url); 11 | const cloudcmdPath = join(__dirname, '..', '..'); 12 | 13 | const modulesPath = join(cloudcmdPath, 'json', 'modules.json'); 14 | const localModules = require(modulesPath); 15 | const modulas = require(`../../server/modulas`); 16 | 17 | const {request} = serveOnce(cloudcmd, { 18 | config: { 19 | auth: false, 20 | dropbox: false, 21 | }, 22 | }); 23 | 24 | test('cloudcmd: modules', async (t) => { 25 | const modules = { 26 | data: { 27 | FilePicker: { 28 | key: 'hello', 29 | }, 30 | }, 31 | }; 32 | 33 | const options = { 34 | modules, 35 | }; 36 | 37 | const expected = { 38 | ...localModules, 39 | ...modules, 40 | }; 41 | 42 | const {body} = await request.get(`/json/modules.json`, { 43 | type: 'json', 44 | options, 45 | }); 46 | 47 | t.deepEqual(body, expected); 48 | t.end(); 49 | }); 50 | 51 | test('cloudcmd: modules: wrong route', async (t) => { 52 | const modules = { 53 | hello: 'world', 54 | }; 55 | 56 | const options = { 57 | modules, 58 | }; 59 | 60 | const expected = { 61 | ...localModules, 62 | ...modules, 63 | }; 64 | 65 | const {body} = await request.get(`/package.json`, { 66 | type: 'json', 67 | options, 68 | }); 69 | 70 | t.notDeepEqual(body, expected, 'should not be equal'); 71 | t.end(); 72 | }); 73 | 74 | test('cloudcmd: modules: no', (t) => { 75 | const fn = modulas(); 76 | const url = '/json/modules.json'; 77 | const send = stub(); 78 | 79 | fn({url}, { 80 | send, 81 | }); 82 | 83 | t.calledWith(send, [localModules], 'should have been called with modules'); 84 | t.end(); 85 | }); 86 | -------------------------------------------------------------------------------- /test/server/show-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('supertape'); 4 | const tryCatch = require('try-catch'); 5 | 6 | const showConfig = require('../../server/show-config'); 7 | 8 | test('cloudcmd: show-config: no arguments', (t) => { 9 | const [error] = tryCatch(showConfig); 10 | 11 | t.equal(error.message, 'config could not be empty!', 'should throw when no config'); 12 | t.end(); 13 | }); 14 | 15 | test('cloudcmd: show-config: bad arguments', (t) => { 16 | const [error] = tryCatch(showConfig, 'hello'); 17 | 18 | t.equal(error.message, 'config should be an object!', 'should throw when config not object'); 19 | t.end(); 20 | }); 21 | 22 | test('cloudcmd: show-config: empty: return', (t) => { 23 | t.equal(showConfig({}), '', 'should return string'); 24 | t.end(); 25 | }); 26 | 27 | test('cloudcmd: show-config: return', (t) => { 28 | const config = { 29 | hello: 'world', 30 | }; 31 | 32 | const result = [ 33 | '+-------+--------------------------------+\n', 34 | '| hello | world |\n', 35 | '+-------+--------------------------------+\n', 36 | ].join(''); 37 | 38 | t.equal(showConfig(config), result, 'should return table'); 39 | t.end(); 40 | }); 41 | -------------------------------------------------------------------------------- /tmpl/fs/file.hbs: -------------------------------------------------------------------------------- 1 | <{{ tag }} {{ attribute }}class="{{ className }}"> 2 | 3 | {{ name }} 4 | {{ size }} 5 | {{ date }} 6 | {{ owner }} 7 | {{ mode }} 8 | -------------------------------------------------------------------------------- /tmpl/fs/link.hbs: -------------------------------------------------------------------------------- 1 | {{ name }} -------------------------------------------------------------------------------- /tmpl/fs/panel.hbs: -------------------------------------------------------------------------------- 1 |
{{ content }}
2 | -------------------------------------------------------------------------------- /tmpl/fs/path.hbs: -------------------------------------------------------------------------------- 1 |
{{ path }}
-------------------------------------------------------------------------------- /tmpl/fs/pathLink.hbs: -------------------------------------------------------------------------------- 1 | {{ name }}{{ slash }} -------------------------------------------------------------------------------- /tmpl/upload.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tmpl/view/media.hbs: -------------------------------------------------------------------------------- 1 |
2 | <{{ type }} data-name="js-media" src="{{ src }}" controls autoplay> 3 |

{{ name }}

4 |
-------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {merge} = require('webpack-merge'); 4 | 5 | const htmlConfig = require('./.webpack/html'); 6 | const cssConfig = require('./.webpack/css'); 7 | const jsConfig = require('./.webpack/js'); 8 | 9 | module.exports = merge([ 10 | jsConfig, 11 | htmlConfig, 12 | cssConfig, 13 | ]); 14 | --------------------------------------------------------------------------------