├── .npmrc ├── docs ├── screenshots │ ├── basic-auth.png │ ├── directory-list.png │ ├── browser_with_remove.png │ └── terminal-screenshot.png └── CONTRIBUTING.MD ├── lib ├── helper │ ├── utils.js │ ├── index.js │ ├── network.js │ ├── html.js │ └── style.css └── middleware │ ├── file-remove.js │ ├── auth.js │ ├── file-upload.js │ ├── index.js │ └── directory.js ├── .eslintrc.json ├── .github ├── workflows │ └── release.yml └── FUNDING.yml ├── LICENSE ├── package.json ├── .gitignore ├── bin └── index.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | # .npmrc 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /docs/screenshots/basic-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-root/directory-serve/HEAD/docs/screenshots/basic-auth.png -------------------------------------------------------------------------------- /docs/screenshots/directory-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-root/directory-serve/HEAD/docs/screenshots/directory-list.png -------------------------------------------------------------------------------- /docs/screenshots/browser_with_remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-root/directory-serve/HEAD/docs/screenshots/browser_with_remove.png -------------------------------------------------------------------------------- /docs/screenshots/terminal-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-root/directory-serve/HEAD/docs/screenshots/terminal-screenshot.png -------------------------------------------------------------------------------- /lib/helper/utils.js: -------------------------------------------------------------------------------- 1 | const appendSlash = (str) => (str[str.length - 1] === '/' ? str : `${str}/`); 2 | 3 | module.exports = { 4 | appendSlash, 5 | }; 6 | -------------------------------------------------------------------------------- /lib/helper/index.js: -------------------------------------------------------------------------------- 1 | const { getNetworkAddress } = require('./network'); 2 | const { appendSlash } = require('./utils'); 3 | const { createHtmlResponse, uploadFileResponse, generateDeleteButton } = require('./html'); 4 | 5 | module.exports = { 6 | getNetworkAddress, 7 | appendSlash, 8 | createHtmlResponse, 9 | uploadFileResponse, 10 | generateDeleteButton, 11 | }; 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": "airbnb-base", 9 | "overrides": [], 10 | "parserOptions": { 11 | "ecmaVersion": "latest", 12 | "ecmaFeatures ": { 13 | "globalReturn": true 14 | } 15 | }, 16 | "rules": { 17 | "no-else-return": "off", 18 | "no-console": "off" 19 | } 20 | } -------------------------------------------------------------------------------- /lib/middleware/file-remove.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const { appendSlash } = require('../helper'); 3 | 4 | const fileRemove = async (req, res, { path, file } = {}) => { 5 | await new Promise((resolve, reject) => { 6 | const filePath = appendSlash(path) + file; 7 | fs.remove(filePath, (err) => { 8 | if (err) { 9 | reject(err); 10 | } else { 11 | resolve(); 12 | } 13 | }); 14 | }); 15 | return res.redirect('/'); 16 | }; 17 | 18 | module.exports = { 19 | fileRemove, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/helper/network.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-continue */ 2 | /* eslint-disable consistent-return */ 3 | /* eslint-disable no-restricted-syntax */ 4 | 5 | const os = require('os'); 6 | 7 | const networkInterfaces = os.networkInterfaces(); 8 | 9 | const getNetworkAddress = () => { 10 | for (const interfaceDetails of Object.values(networkInterfaces)) { 11 | if (!interfaceDetails) { continue; } 12 | for (const details of interfaceDetails) { 13 | const { address, family, internal } = details; 14 | if ((family === 'IPv4' || family === 4) && !internal) { 15 | return address; 16 | } 17 | } 18 | } 19 | }; 20 | 21 | module.exports = { 22 | getNetworkAddress, 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: 3 | # push: 4 | # branches: 5 | # - "main" 6 | release: 7 | types: [created] 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup Node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: '16.x' 18 | registry-url: 'https://registry.npmjs.org' 19 | - name: Install dependencies and build 🔧 20 | run: npm install 21 | - name: Publish package on NPM 22 | run: npm publish --access=public 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /lib/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const authMiddleware = (req, res, next, auth) => { 2 | if (!auth || Object.keys(auth).length === 0 || !auth.username) { 3 | return next(); 4 | } 5 | // parse login and password from headers 6 | const b64auth = (req.headers.authorization || '').split(' ')[1] || ''; 7 | const [username, password] = Buffer.from(b64auth, 'base64').toString().split(':'); 8 | 9 | if (username && username === auth.username 10 | && (!auth.password || (password && password === auth.password))) { 11 | return next(); 12 | } 13 | 14 | res.set('WWW-Authenticate', 'Basic realm="401"'); 15 | return res.status(401).send('Authentication required.'); 16 | }; 17 | module.exports = { 18 | authMiddleware, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ["https://www.buymeacoffee.com/abhisawzm"] 14 | -------------------------------------------------------------------------------- /lib/middleware/file-upload.js: -------------------------------------------------------------------------------- 1 | const formidable = require('formidable'); 2 | const fs = require('fs-extra'); 3 | const { appendSlash } = require('../helper'); 4 | 5 | const fileUpload = async (req, res, { path } = {}) => { 6 | const form = new formidable.IncomingForm(); 7 | await new Promise((resolve, reject) => { 8 | form.parse(req, (err, fields, files) => { 9 | if (err) throw err; 10 | const oldpath = files.filetoupload.filepath; 11 | let newPath = appendSlash(path) + files.filetoupload.originalFilename; 12 | if (fs.existsSync(newPath)) { 13 | newPath = `${appendSlash(path) + new Date().getTime()}_${files.filetoupload.originalFilename}`; 14 | } 15 | try { 16 | fs.moveSync(oldpath, newPath); 17 | } catch (error) { 18 | reject(error); 19 | } 20 | resolve(); 21 | }); 22 | }); 23 | return res.redirect('/'); 24 | }; 25 | 26 | module.exports = { 27 | fileUpload, 28 | }; 29 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | This project adheres to the [Contributor Covenant Code of Conduct](http://contributor-covenant.org/). 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 15 | 3. Pull request should only contain one issues at a time. 16 | 17 | 18 | # Maintainers 19 | 20 | Directory serve is maintained by: 21 | 22 | [Abhijith V](https://github.com/abhisawesome/) 23 | 24 | See also the list of [contributors](https://github.com/cube-root/directory-serve/graphs/contributors) who participated in this project. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cube-Root 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/middleware/index.js: -------------------------------------------------------------------------------- 1 | const queryString = require('query-string'); 2 | const { directory } = require('./directory'); 3 | const { fileUpload } = require('./file-upload'); 4 | const { fileRemove } = require('./file-remove'); 5 | const { authMiddleware } = require('./auth'); 6 | 7 | const handler = ( 8 | req, 9 | res, 10 | { 11 | path, uploadFile = true, deleteFile = false, debug = false, 12 | } = {}, 13 | ) => { 14 | if ( 15 | req.query.file 16 | && req.query.delete 17 | && req.query.delete.toLowerCase() === 'true' 18 | && deleteFile 19 | ) { 20 | return fileRemove(req, res, { 21 | // path: `${path.replace(/[/]$/, '')}/${req.path.replace(/^[/]/, '')}`, 22 | path, 23 | file: decodeURIComponent(req.query.file), 24 | }); 25 | } 26 | 27 | if (req.method === 'POST' && uploadFile) { 28 | const query = queryString.parse(req.url); 29 | if (query && !query.path) { 30 | return res.status(500).send('Invalid path'); 31 | } 32 | return fileUpload(req, res, { 33 | path: query.path, 34 | originalPath: path, 35 | uploadFile, 36 | }); 37 | } 38 | 39 | return directory(req, res, { 40 | path, 41 | uploadFile, 42 | deleteFile, 43 | debug, 44 | }); 45 | }; 46 | 47 | module.exports = { 48 | handler, 49 | authMiddleware, 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directory-serve", 3 | "version": "1.3.6", 4 | "description": "Command line tool to share the directory", 5 | "main": "./bin/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon bin/index.js" 9 | }, 10 | "keywords": [ 11 | "file sharing", 12 | "http", 13 | "https", 14 | "cli", 15 | "http-server", 16 | "https-server", 17 | "android", 18 | "ios", 19 | "mobile", 20 | "cli sharing", 21 | "directory sharing", 22 | "file serving", 23 | "directory serving", 24 | "mobile file sharing", 25 | "share clipboard", 26 | "receive file", 27 | "send file", 28 | "static", 29 | "file", 30 | "server" 31 | ], 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/cube-root/directory-serve.git" 35 | }, 36 | "engines": { 37 | "node": ">=12.0" 38 | }, 39 | "directories": { 40 | "bin": "./bin", 41 | "lib": "./lib" 42 | }, 43 | "bin": { 44 | "directory-serve": "./bin/index.js" 45 | }, 46 | "author": "Abhijith v (abhijithababhijith@gmail.com)", 47 | "license": "ISC", 48 | "bugs": { 49 | "url": "https://github.com/cube-root/directory-serve/issues" 50 | }, 51 | "homepage": "https://github.com/cube-root/directory-serve#readme", 52 | "dependencies": { 53 | "express": "^4.18.2", 54 | "formidable": "^2.0.1", 55 | "fs-extra": "^10.1.0", 56 | "os": "^0.1.2", 57 | "parseurl": "^1.3.3", 58 | "qrcode-terminal": "^0.12.0", 59 | "query-string": "^7.1.1", 60 | "send": "^0.18.0", 61 | "yargs": "^17.6.0" 62 | }, 63 | "devDependencies": { 64 | "eslint": "^8.25.0", 65 | "eslint-config-airbnb-base": "^15.0.0", 66 | "eslint-plugin-import": "^2.26.0", 67 | "nodemon": "^2.0.20" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # File upload testing folder 107 | file-upload-test-folder/* -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const yargs = require('yargs'); 3 | const fs = require('fs-extra'); 4 | const qrcode = require('qrcode-terminal'); 5 | const express = require('express'); 6 | const { getNetworkAddress } = require('../lib/helper'); 7 | const { handler, authMiddleware } = require('../lib/middleware'); 8 | 9 | const app = express(); 10 | const yargsMessage = `directory-serve 11 | Serve directory/file 12 | 13 | Usage : directory-serve [path] [args] 14 | 15 | Options 16 | -p ............. Port 17 | 18 | -u ............. Restrict upload file on client :default value is false 19 | 20 | --username ..... Client auth username 21 | 22 | 23 | --password ..... Client auth password 24 | 25 | --delete ..... Delete file/folder 26 | 27 | --debug ..... Debug mode 28 | * To serve a directory 29 | directory-serve /path-of-directory 30 | 31 | * To serve a file 32 | directory-serve /path-of-file 33 | 34 | 35 | `; 36 | 37 | const options = yargs 38 | .usage(yargsMessage) 39 | .option('p', { 40 | default: 8989, 41 | alias: 'port', 42 | describe: 'Change default port', 43 | type: 'integer', 44 | demandOption: false, 45 | }) 46 | .option('u', { 47 | default: true, 48 | alias: 'uploadFile', 49 | describe: 'File upload mode', 50 | type: 'boolean', 51 | }) 52 | .options('username', { 53 | default: undefined, 54 | describe: 'Client auth username', 55 | type: 'string', 56 | demandOption: false, 57 | }) 58 | .options('password', { 59 | default: undefined, 60 | describe: 'Client auth password', 61 | type: 'string', 62 | demandOption: false, 63 | }) 64 | .options('delete', { 65 | default: false, 66 | alias: 'deleteFile', 67 | describe: 'Delete file/folder', 68 | type: 'boolean', 69 | demandOption: false, 70 | }) 71 | .options('debug', { 72 | default: false, 73 | describe: 'Debug mode', 74 | type: 'boolean', 75 | demandOption: false, 76 | }) 77 | .help(true).argv; 78 | 79 | const { 80 | uploadFile, username, password, deleteFile, debug, 81 | } = options; 82 | let path = options._[0]; 83 | if (!path) { 84 | console.log('Please specify path'); 85 | process.exit(); 86 | } 87 | 88 | /** 89 | * check path exist 90 | */ 91 | if (!fs.existsSync(path)) { 92 | console.log('Directory not found'); 93 | process.exit(); 94 | } 95 | 96 | /** 97 | * Check is file 98 | */ 99 | 100 | const isFile = fs.lstatSync(path).isFile(); 101 | let fileName; 102 | if (isFile) { 103 | const directoryPath = path.substring(0, path.lastIndexOf('/') + 1); 104 | fileName = path.substring(path.lastIndexOf('/') + 1, path.length); 105 | path = directoryPath; 106 | } 107 | 108 | /** 109 | * Auth 110 | */ 111 | app.use((req, res, next) => authMiddleware(req, res, next, { 112 | username, 113 | password, 114 | })); 115 | /** 116 | * SERVER 117 | */ 118 | app.use((req, res) => handler(req, res, { 119 | path, 120 | uploadFile, 121 | deleteFile, 122 | debug, 123 | })); 124 | 125 | app.listen(options.port, () => { 126 | let message = 'Scan the QR Code to access directory'; 127 | let file = ''; 128 | if (isFile) { 129 | message = 'Scan the QR Code to access file on your phone'; 130 | file = `/${fileName}`; 131 | } 132 | console.log(message); 133 | const url = `http://${getNetworkAddress()}:${options.port}${file}`; 134 | console.log(url); 135 | qrcode.generate(url, { small: true }); 136 | console.log('NOTE: Devices should be in same network'); 137 | console.log('Press ctrl+c to stop sharing'); 138 | }); 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Directory Serve 2 | 3 | Directory Serve - Open source CLI to send and receive file | Product Hunt 4 | 5 | Directory serve is a CLI library for sending and receiving a file from your android and IOS devices. 6 | 7 | ## Installation 8 | 9 | This is a [Node.js](https://nodejs.org/en/) module available through the 10 | [npm registry](https://www.npmjs.com/). Installation is done using the 11 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 12 | 13 | ```bash 14 | $ npm install -g directory-serve 15 | ``` 16 | 17 | ## Help 18 | 19 | ```bash 20 | npx directory-serve --help 21 | ``` 22 | 23 | ## Usage 24 | 25 | After installing globally 26 | 27 | ```bash 28 | directory-serve /path-of-directory 29 | ``` 30 | 31 | or 32 | 33 | Directly use the command 34 | 35 | ```bash 36 | npx directory-serve /path-of-directory 37 | ``` 38 | 39 | or 40 | 41 | ```bash 42 | npx directory-serve /path-to-file 43 | ``` 44 | 45 | ## Arguments 46 | 47 | | options | default | description | Example | 48 | | :------: | :-------: | :-----------------------------: | :-------------------------------------------------------------------------------------: | 49 | | u | true | Restrict upload file option | `npx directory-serve /path-of-directory -u=false` | 50 | | p | 8989 | Change the port | `npx directory-serve /path-of-directory -p=3000` | 51 | | help | | Help | `npx directory-serve --help ` | 52 | | username | undefined | Client auth username | `npx directory-serve /path-of-directory --username=my_username ` | 53 | | password | undefined | Client auth password (optional) | `npx directory-serve /path-of-directory --username=my_username --password=my_password ` | 54 | | delete | false | To delete file/folder | `npx directory-serve /path-of-directory --delete=true` | 55 | | debug | false | Debug mode | `npx directory-serve /path-of-directory --delete=true --debug=true` | 56 | 57 | ## Examples 58 | 59 | ```bash 60 | npx directory-serve . 61 | ``` 62 | 63 | ```bash 64 | npx directory-serve ~/Desktop 65 | ``` 66 | 67 | ```bash 68 | npx directory-serve ~/Desktop/my_image.png 69 | ``` 70 | 71 | ```bash 72 | npx directory-serve ~/Desktop -p=3000 --username=test --password=password 73 | ``` 74 | 75 | ## For Developing 76 | 77 | ### prerequisite 78 | 79 | 1. Node (>=12.0) 80 | 81 |
82 | clone the repo and follow the commands 83 | 84 | ```bash 85 | git clone https://github.com/cube-root/directory-serve.git 86 | ``` 87 | 88 | ```bash 89 | npm i 90 | ``` 91 | 92 | ```bash 93 | npm run dev /path-of-directory 94 | ``` 95 | 96 | ## For Contributing 97 | 98 | [Contribution Guide](/docs/CONTRIBUTING.MD) 99 | 100 | ## Screenshot 101 | 102 | ### CLI 103 | 104 | ![screenshot](/docs/screenshots/terminal-screenshot.png?raw=true "Directory serve") 105 | 106 | ### Client 107 | 108 | ![screenshot](/docs/screenshots/directory-list.png?raw=true) 109 | ![screenshot](/docs/screenshots/browser_with_remove.png?raw=true) 110 | 111 | ### Client Auth 112 | 113 | ![screenshot](/docs/screenshots/basic-auth.png?raw=true) 114 | -------------------------------------------------------------------------------- /lib/middleware/directory.js: -------------------------------------------------------------------------------- 1 | const send = require('send'); 2 | const parseUrl = require('parseurl'); 3 | const fs = require('fs-extra'); 4 | const { 5 | appendSlash, 6 | createHtmlResponse, 7 | uploadFileResponse, 8 | generateDeleteButton, 9 | } = require('../helper'); 10 | 11 | const directory = ( 12 | req, 13 | res, 14 | { 15 | path, uploadFile = true, deleteFile = false, debug = false, 16 | } = {}, 17 | ) => { 18 | const stream = send(req, parseUrl(req).pathname, { 19 | index: false, 20 | root: path, 21 | dotfiles: 'allow', 22 | }); 23 | stream.on('directory', async (resp, dirPath) => { 24 | const directoryPath = appendSlash(dirPath); 25 | let htmlResponse = ''; 26 | /** 27 | * File upload html append 28 | */ 29 | if (fs.lstatSync(directoryPath).isDirectory() && uploadFile) { 30 | htmlResponse += uploadFileResponse(dirPath, path); 31 | } 32 | await new Promise((resolve) => { 33 | fs.readdir(directoryPath, (err, files) => { 34 | if (err) { 35 | console.log(err); 36 | resolve('
Error
'); 37 | } 38 | htmlResponse 39 | += '

Directory List

'; 72 | resolve(htmlResponse); 73 | }); 74 | }); 75 | resp.setHeader('Content-Type', 'text/html; charset=UTF-8'); 76 | return resp.end(createHtmlResponse(htmlResponse, { debug })); 77 | }); 78 | stream.pipe(res); 79 | }; 80 | 81 | module.exports = { 82 | directory, 83 | }; 84 | -------------------------------------------------------------------------------- /lib/helper/html.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | 3 | const cssLoader = (debug) => { 4 | try { 5 | return ``; 6 | } catch (error) { 7 | if (debug) { 8 | console.log('Failed to load stylesheet'); 9 | console.log(error); 10 | } 11 | } 12 | return ''; 13 | }; 14 | const createHtmlResponse = (body, { debug = false } = {}) => '\n' 15 | + '\n' 16 | + `\n${ 17 | // + '' 18 | // + '' 19 | cssLoader(debug) 20 | }` 33 | + '\n' 34 | + '' 35 | + '\n' 36 | + '\n' 37 | + `` 41 | + `
${body}
\n` 42 | + '\n' 43 | + '\n'; 44 | 45 | const uploadFileResponse = (path) => '
' 46 | + `

Upload files to ${path}

` 47 | + `
` 48 | + '' 51 | + '
' 54 | + '
'; 55 | 56 | const generateDeleteButton = (file, deleteFile, label = 'Remove') => (!deleteFile === true 57 | ? '' 58 | : `` 61 | + '' 62 | + `${label}` 63 | + ''); 64 | 65 | module.exports = { 66 | createHtmlResponse, 67 | uploadFileResponse, 68 | generateDeleteButton, 69 | }; 70 | -------------------------------------------------------------------------------- /lib/helper/style.css: -------------------------------------------------------------------------------- 1 | /* ! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com */ 2 | 3 | /* 4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 6 | */ 7 | 8 | *, 9 | ::before, 10 | ::after { 11 | box-sizing: border-box; 12 | /* 1 */ 13 | border-width: 0; 14 | /* 2 */ 15 | border-style: solid; 16 | /* 2 */ 17 | border-color: #e5e7eb; 18 | /* 2 */ 19 | } 20 | 21 | ::before, 22 | ::after { 23 | --tw-content: ""; 24 | } 25 | 26 | /* 27 | 1. Use a consistent sensible line-height in all browsers. 28 | 2. Prevent adjustments of font size after orientation changes in iOS. 29 | 3. Use a more readable tab size. 30 | 4. Use the user's configured `sans` font-family by default. 31 | 5. Use the user's configured `sans` font-feature-settings by default. 32 | */ 33 | 34 | html { 35 | line-height: 1.5; 36 | /* 1 */ 37 | -webkit-text-size-adjust: 100%; 38 | /* 2 */ 39 | -moz-tab-size: 4; 40 | /* 3 */ 41 | tab-size: 4; 42 | /* 3 */ 43 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 44 | "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, 45 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 46 | /* 4 */ 47 | font-feature-settings: normal; 48 | /* 5 */ 49 | } 50 | 51 | /* 52 | 1. Remove the margin in all browsers. 53 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 54 | */ 55 | 56 | body { 57 | margin: 0; 58 | /* 1 */ 59 | line-height: inherit; 60 | /* 2 */ 61 | } 62 | 63 | /* 64 | 1. Add the correct height in Firefox. 65 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 66 | 3. Ensure horizontal rules are visible by default. 67 | */ 68 | 69 | hr { 70 | height: 0; 71 | /* 1 */ 72 | color: inherit; 73 | /* 2 */ 74 | border-top-width: 1px; 75 | /* 3 */ 76 | } 77 | 78 | /* 79 | Add the correct text decoration in Chrome, Edge, and Safari. 80 | */ 81 | 82 | abbr:where([title]) { 83 | -webkit-text-decoration: underline dotted; 84 | text-decoration: underline dotted; 85 | } 86 | 87 | /* 88 | Remove the default font size and weight for headings. 89 | */ 90 | 91 | h1, 92 | h2, 93 | h3, 94 | h4, 95 | h5, 96 | h6 { 97 | font-size: inherit; 98 | font-weight: inherit; 99 | } 100 | 101 | /* 102 | Reset links to optimize for opt-in styling instead of opt-out. 103 | */ 104 | 105 | a { 106 | color: inherit; 107 | text-decoration: inherit; 108 | } 109 | 110 | /* 111 | Add the correct font weight in Edge and Safari. 112 | */ 113 | 114 | b, 115 | strong { 116 | font-weight: bolder; 117 | } 118 | 119 | /* 120 | 1. Use the user's configured `mono` font family by default. 121 | 2. Correct the odd `em` font sizing in all browsers. 122 | */ 123 | 124 | code, 125 | kbd, 126 | samp, 127 | pre { 128 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 129 | "Liberation Mono", "Courier New", monospace; 130 | /* 1 */ 131 | font-size: 1em; 132 | /* 2 */ 133 | } 134 | 135 | /* 136 | Add the correct font size in all browsers. 137 | */ 138 | 139 | small { 140 | font-size: 80%; 141 | } 142 | 143 | /* 144 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 145 | */ 146 | 147 | sub, 148 | sup { 149 | font-size: 75%; 150 | line-height: 0; 151 | position: relative; 152 | vertical-align: baseline; 153 | } 154 | 155 | sub { 156 | bottom: -0.25em; 157 | } 158 | 159 | sup { 160 | top: -0.5em; 161 | } 162 | 163 | /* 164 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 165 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 166 | 3. Remove gaps between table borders by default. 167 | */ 168 | 169 | table { 170 | text-indent: 0; 171 | /* 1 */ 172 | border-color: inherit; 173 | /* 2 */ 174 | border-collapse: collapse; 175 | /* 3 */ 176 | } 177 | 178 | /* 179 | 1. Change the font styles in all browsers. 180 | 2. Remove the margin in Firefox and Safari. 181 | 3. Remove default padding in all browsers. 182 | */ 183 | 184 | button, 185 | input, 186 | optgroup, 187 | select, 188 | textarea { 189 | font-family: inherit; 190 | /* 1 */ 191 | font-size: 100%; 192 | /* 1 */ 193 | font-weight: inherit; 194 | /* 1 */ 195 | line-height: inherit; 196 | /* 1 */ 197 | color: inherit; 198 | /* 1 */ 199 | margin: 0; 200 | /* 2 */ 201 | padding: 0; 202 | /* 3 */ 203 | } 204 | 205 | /* 206 | Remove the inheritance of text transform in Edge and Firefox. 207 | */ 208 | 209 | button, 210 | select { 211 | text-transform: none; 212 | } 213 | 214 | /* 215 | 1. Correct the inability to style clickable types in iOS and Safari. 216 | 2. Remove default button styles. 217 | */ 218 | 219 | button, 220 | [type="button"], 221 | [type="reset"], 222 | [type="submit"] { 223 | -webkit-appearance: button; 224 | /* 1 */ 225 | background-color: transparent; 226 | /* 2 */ 227 | background-image: none; 228 | /* 2 */ 229 | } 230 | 231 | /* 232 | Use the modern Firefox focus style for all focusable elements. 233 | */ 234 | 235 | :-moz-focusring { 236 | outline: auto; 237 | } 238 | 239 | /* 240 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 241 | */ 242 | 243 | :-moz-ui-invalid { 244 | box-shadow: none; 245 | } 246 | 247 | /* 248 | Add the correct vertical alignment in Chrome and Firefox. 249 | */ 250 | 251 | progress { 252 | vertical-align: baseline; 253 | } 254 | 255 | /* 256 | Correct the cursor style of increment and decrement buttons in Safari. 257 | */ 258 | 259 | ::-webkit-inner-spin-button, 260 | ::-webkit-outer-spin-button { 261 | height: auto; 262 | } 263 | 264 | /* 265 | 1. Correct the odd appearance in Chrome and Safari. 266 | 2. Correct the outline style in Safari. 267 | */ 268 | 269 | [type="search"] { 270 | -webkit-appearance: textfield; 271 | /* 1 */ 272 | outline-offset: -2px; 273 | /* 2 */ 274 | } 275 | 276 | /* 277 | Remove the inner padding in Chrome and Safari on macOS. 278 | */ 279 | 280 | ::-webkit-search-decoration { 281 | -webkit-appearance: none; 282 | } 283 | 284 | /* 285 | 1. Correct the inability to style clickable types in iOS and Safari. 286 | 2. Change font properties to `inherit` in Safari. 287 | */ 288 | 289 | ::-webkit-file-upload-button { 290 | -webkit-appearance: button; 291 | /* 1 */ 292 | font: inherit; 293 | /* 2 */ 294 | } 295 | 296 | /* 297 | Add the correct display in Chrome and Safari. 298 | */ 299 | 300 | summary { 301 | display: list-item; 302 | } 303 | 304 | /* 305 | Removes the default spacing and border for appropriate elements. 306 | */ 307 | 308 | blockquote, 309 | dl, 310 | dd, 311 | h1, 312 | h2, 313 | h3, 314 | h4, 315 | h5, 316 | h6, 317 | hr, 318 | figure, 319 | p, 320 | pre { 321 | margin: 0; 322 | } 323 | 324 | fieldset { 325 | margin: 0; 326 | padding: 0; 327 | } 328 | 329 | legend { 330 | padding: 0; 331 | } 332 | 333 | ol, 334 | ul, 335 | menu { 336 | list-style: none; 337 | margin: 0; 338 | padding: 0; 339 | } 340 | 341 | /* 342 | Prevent resizing textareas horizontally by default. 343 | */ 344 | 345 | textarea { 346 | resize: vertical; 347 | } 348 | 349 | /* 350 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 351 | 2. Set the default placeholder color to the user's configured gray 400 color. 352 | */ 353 | 354 | input::placeholder, 355 | textarea::placeholder { 356 | opacity: 1; 357 | /* 1 */ 358 | color: #9ca3af; 359 | /* 2 */ 360 | } 361 | 362 | /* 363 | Set the default cursor for buttons. 364 | */ 365 | 366 | button, 367 | [role="button"] { 368 | cursor: pointer; 369 | } 370 | 371 | /* 372 | Make sure disabled buttons don't get the pointer cursor. 373 | */ 374 | 375 | :disabled { 376 | cursor: default; 377 | } 378 | 379 | /* 380 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 381 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 382 | This can trigger a poorly considered lint error in some tools but is included by design. 383 | */ 384 | 385 | img, 386 | svg, 387 | video, 388 | canvas, 389 | audio, 390 | iframe, 391 | embed, 392 | object { 393 | display: block; 394 | /* 1 */ 395 | vertical-align: middle; 396 | /* 2 */ 397 | } 398 | 399 | /* 400 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 401 | */ 402 | 403 | img, 404 | video { 405 | max-width: 100%; 406 | height: auto; 407 | } 408 | 409 | /* Make elements with the HTML hidden attribute stay hidden by default */ 410 | 411 | [hidden] { 412 | display: none; 413 | } 414 | 415 | *, 416 | ::before, 417 | ::after { 418 | --tw-border-spacing-x: 0; 419 | --tw-border-spacing-y: 0; 420 | --tw-translate-x: 0; 421 | --tw-translate-y: 0; 422 | --tw-rotate: 0; 423 | --tw-skew-x: 0; 424 | --tw-skew-y: 0; 425 | --tw-scale-x: 1; 426 | --tw-scale-y: 1; 427 | --tw-pan-x: ; 428 | --tw-pan-y: ; 429 | --tw-pinch-zoom: ; 430 | --tw-scroll-snap-strictness: proximity; 431 | --tw-ordinal: ; 432 | --tw-slashed-zero: ; 433 | --tw-numeric-figure: ; 434 | --tw-numeric-spacing: ; 435 | --tw-numeric-fraction: ; 436 | --tw-ring-inset: ; 437 | --tw-ring-offset-width: 0px; 438 | --tw-ring-offset-color: #fff; 439 | --tw-ring-color: rgb(59 130 246 / 0.5); 440 | --tw-ring-offset-shadow: 0 0 #0000; 441 | --tw-ring-shadow: 0 0 #0000; 442 | --tw-shadow: 0 0 #0000; 443 | --tw-shadow-colored: 0 0 #0000; 444 | --tw-blur: ; 445 | --tw-brightness: ; 446 | --tw-contrast: ; 447 | --tw-grayscale: ; 448 | --tw-hue-rotate: ; 449 | --tw-invert: ; 450 | --tw-saturate: ; 451 | --tw-sepia: ; 452 | --tw-drop-shadow: ; 453 | --tw-backdrop-blur: ; 454 | --tw-backdrop-brightness: ; 455 | --tw-backdrop-contrast: ; 456 | --tw-backdrop-grayscale: ; 457 | --tw-backdrop-hue-rotate: ; 458 | --tw-backdrop-invert: ; 459 | --tw-backdrop-opacity: ; 460 | --tw-backdrop-saturate: ; 461 | --tw-backdrop-sepia: ; 462 | } 463 | 464 | ::-webkit-backdrop { 465 | --tw-border-spacing-x: 0; 466 | --tw-border-spacing-y: 0; 467 | --tw-translate-x: 0; 468 | --tw-translate-y: 0; 469 | --tw-rotate: 0; 470 | --tw-skew-x: 0; 471 | --tw-skew-y: 0; 472 | --tw-scale-x: 1; 473 | --tw-scale-y: 1; 474 | --tw-pan-x: ; 475 | --tw-pan-y: ; 476 | --tw-pinch-zoom: ; 477 | --tw-scroll-snap-strictness: proximity; 478 | --tw-ordinal: ; 479 | --tw-slashed-zero: ; 480 | --tw-numeric-figure: ; 481 | --tw-numeric-spacing: ; 482 | --tw-numeric-fraction: ; 483 | --tw-ring-inset: ; 484 | --tw-ring-offset-width: 0px; 485 | --tw-ring-offset-color: #fff; 486 | --tw-ring-color: rgb(59 130 246 / 0.5); 487 | --tw-ring-offset-shadow: 0 0 #0000; 488 | --tw-ring-shadow: 0 0 #0000; 489 | --tw-shadow: 0 0 #0000; 490 | --tw-shadow-colored: 0 0 #0000; 491 | --tw-blur: ; 492 | --tw-brightness: ; 493 | --tw-contrast: ; 494 | --tw-grayscale: ; 495 | --tw-hue-rotate: ; 496 | --tw-invert: ; 497 | --tw-saturate: ; 498 | --tw-sepia: ; 499 | --tw-drop-shadow: ; 500 | --tw-backdrop-blur: ; 501 | --tw-backdrop-brightness: ; 502 | --tw-backdrop-contrast: ; 503 | --tw-backdrop-grayscale: ; 504 | --tw-backdrop-hue-rotate: ; 505 | --tw-backdrop-invert: ; 506 | --tw-backdrop-opacity: ; 507 | --tw-backdrop-saturate: ; 508 | --tw-backdrop-sepia: ; 509 | } 510 | 511 | ::backdrop { 512 | --tw-border-spacing-x: 0; 513 | --tw-border-spacing-y: 0; 514 | --tw-translate-x: 0; 515 | --tw-translate-y: 0; 516 | --tw-rotate: 0; 517 | --tw-skew-x: 0; 518 | --tw-skew-y: 0; 519 | --tw-scale-x: 1; 520 | --tw-scale-y: 1; 521 | --tw-pan-x: ; 522 | --tw-pan-y: ; 523 | --tw-pinch-zoom: ; 524 | --tw-scroll-snap-strictness: proximity; 525 | --tw-ordinal: ; 526 | --tw-slashed-zero: ; 527 | --tw-numeric-figure: ; 528 | --tw-numeric-spacing: ; 529 | --tw-numeric-fraction: ; 530 | --tw-ring-inset: ; 531 | --tw-ring-offset-width: 0px; 532 | --tw-ring-offset-color: #fff; 533 | --tw-ring-color: rgb(59 130 246 / 0.5); 534 | --tw-ring-offset-shadow: 0 0 #0000; 535 | --tw-ring-shadow: 0 0 #0000; 536 | --tw-shadow: 0 0 #0000; 537 | --tw-shadow-colored: 0 0 #0000; 538 | --tw-blur: ; 539 | --tw-brightness: ; 540 | --tw-contrast: ; 541 | --tw-grayscale: ; 542 | --tw-hue-rotate: ; 543 | --tw-invert: ; 544 | --tw-saturate: ; 545 | --tw-sepia: ; 546 | --tw-drop-shadow: ; 547 | --tw-backdrop-blur: ; 548 | --tw-backdrop-brightness: ; 549 | --tw-backdrop-contrast: ; 550 | --tw-backdrop-grayscale: ; 551 | --tw-backdrop-hue-rotate: ; 552 | --tw-backdrop-invert: ; 553 | --tw-backdrop-opacity: ; 554 | --tw-backdrop-saturate: ; 555 | --tw-backdrop-sepia: ; 556 | } 557 | 558 | .container { 559 | width: 100%; 560 | } 561 | 562 | @media (min-width: 640px) { 563 | .container { 564 | max-width: 640px; 565 | } 566 | } 567 | 568 | @media (min-width: 768px) { 569 | .container { 570 | max-width: 768px; 571 | } 572 | } 573 | 574 | @media (min-width: 1024px) { 575 | .container { 576 | max-width: 1024px; 577 | } 578 | } 579 | 580 | @media (min-width: 1280px) { 581 | .container { 582 | max-width: 1280px; 583 | } 584 | } 585 | 586 | @media (min-width: 1536px) { 587 | .container { 588 | max-width: 1536px; 589 | } 590 | } 591 | 592 | .sr-only { 593 | position: absolute; 594 | width: 1px; 595 | height: 1px; 596 | padding: 0; 597 | margin: -1px; 598 | overflow: hidden; 599 | clip: rect(0, 0, 0, 0); 600 | white-space: nowrap; 601 | border-width: 0; 602 | } 603 | 604 | .absolute { 605 | position: absolute; 606 | } 607 | 608 | .mx-auto { 609 | margin-left: auto; 610 | margin-right: auto; 611 | } 612 | 613 | .mb-4 { 614 | margin-bottom: 1rem; 615 | } 616 | 617 | .block { 618 | display: block; 619 | } 620 | 621 | .flex { 622 | display: flex; 623 | } 624 | 625 | .inline-flex { 626 | display: inline-flex; 627 | } 628 | 629 | .h-screen { 630 | height: 100vh; 631 | } 632 | 633 | .h-6 { 634 | height: 1.5rem; 635 | } 636 | 637 | .h-4 { 638 | height: 1rem; 639 | } 640 | 641 | .h-5 { 642 | height: 1.25rem; 643 | } 644 | 645 | .w-6 { 646 | width: 1.5rem; 647 | } 648 | 649 | .w-4 { 650 | width: 1rem; 651 | } 652 | 653 | .w-full { 654 | width: 100%; 655 | } 656 | 657 | .w-5 { 658 | width: 1.25rem; 659 | } 660 | 661 | .flex-1 { 662 | flex: 1 1 0%; 663 | } 664 | 665 | .flex-shrink-0 { 666 | flex-shrink: 0; 667 | } 668 | 669 | .flex-col { 670 | flex-direction: column; 671 | } 672 | 673 | .flex-wrap { 674 | flex-wrap: wrap; 675 | } 676 | 677 | .items-center { 678 | align-items: center; 679 | } 680 | 681 | .space-y-4 > :not([hidden]) ~ :not([hidden]) { 682 | --tw-space-y-reverse: 0; 683 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 684 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 685 | } 686 | 687 | .space-x-2 > :not([hidden]) ~ :not([hidden]) { 688 | --tw-space-x-reverse: 0; 689 | margin-right: calc(0.5rem * var(--tw-space-x-reverse)); 690 | margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); 691 | } 692 | 693 | .space-x-1 > :not([hidden]) ~ :not([hidden]) { 694 | --tw-space-x-reverse: 0; 695 | margin-right: calc(0.25rem * var(--tw-space-x-reverse)); 696 | margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); 697 | } 698 | 699 | .space-x-0 > :not([hidden]) ~ :not([hidden]) { 700 | --tw-space-x-reverse: 0; 701 | margin-right: calc(0px * var(--tw-space-x-reverse)); 702 | margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse))); 703 | } 704 | 705 | .overflow-y-auto { 706 | overflow-y: auto; 707 | } 708 | 709 | .whitespace-normal { 710 | white-space: normal; 711 | } 712 | 713 | .rounded-full { 714 | border-radius: 9999px; 715 | } 716 | 717 | .rounded { 718 | border-radius: 0.25rem; 719 | } 720 | 721 | .border-t { 722 | border-top-width: 1px; 723 | } 724 | 725 | .border-b { 726 | border-bottom-width: 1px; 727 | } 728 | 729 | .border-blue-500 { 730 | --tw-border-opacity: 1; 731 | border-color: rgb(59 130 246 / var(--tw-border-opacity)); 732 | } 733 | 734 | .bg-blue-100 { 735 | --tw-bg-opacity: 1; 736 | background-color: rgb(219 234 254 / var(--tw-bg-opacity)); 737 | } 738 | 739 | .bg-blue-50 { 740 | --tw-bg-opacity: 1; 741 | background-color: rgb(239 246 255 / var(--tw-bg-opacity)); 742 | } 743 | 744 | .bg-blue-200 { 745 | --tw-bg-opacity: 1; 746 | background-color: rgb(191 219 254 / var(--tw-bg-opacity)); 747 | } 748 | 749 | .bg-slate-50 { 750 | --tw-bg-opacity: 1; 751 | background-color: rgb(248 250 252 / var(--tw-bg-opacity)); 752 | } 753 | 754 | .bg-purple-600 { 755 | --tw-bg-opacity: 1; 756 | background-color: rgb(147 51 234 / var(--tw-bg-opacity)); 757 | } 758 | 759 | .px-4 { 760 | padding-left: 1rem; 761 | padding-right: 1rem; 762 | } 763 | 764 | .py-3 { 765 | padding-top: 0.75rem; 766 | padding-bottom: 0.75rem; 767 | } 768 | 769 | .py-6 { 770 | padding-top: 1.5rem; 771 | padding-bottom: 1.5rem; 772 | } 773 | 774 | .px-3 { 775 | padding-left: 0.75rem; 776 | padding-right: 0.75rem; 777 | } 778 | 779 | .py-1 { 780 | padding-top: 0.25rem; 781 | padding-bottom: 0.25rem; 782 | } 783 | 784 | .px-2 { 785 | padding-left: 0.5rem; 786 | padding-right: 0.5rem; 787 | } 788 | 789 | .py-12 { 790 | padding-top: 3rem; 791 | padding-bottom: 3rem; 792 | } 793 | 794 | .py-2 { 795 | padding-top: 0.5rem; 796 | padding-bottom: 0.5rem; 797 | } 798 | 799 | .text-center { 800 | text-align: center; 801 | } 802 | 803 | .font-sans { 804 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 805 | "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, 806 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 807 | } 808 | 809 | .text-sm { 810 | font-size: 0.875rem; 811 | line-height: 1.25rem; 812 | } 813 | 814 | .text-xl { 815 | font-size: 1.25rem; 816 | line-height: 1.75rem; 817 | } 818 | 819 | .text-lg { 820 | font-size: 1.125rem; 821 | line-height: 1.75rem; 822 | } 823 | 824 | .font-bold { 825 | font-weight: 700; 826 | } 827 | 828 | .font-medium { 829 | font-weight: 500; 830 | } 831 | 832 | .text-blue-700 { 833 | --tw-text-opacity: 1; 834 | color: rgb(29 78 216 / var(--tw-text-opacity)); 835 | } 836 | 837 | .text-slate-500 { 838 | --tw-text-opacity: 1; 839 | color: rgb(100 116 139 / var(--tw-text-opacity)); 840 | } 841 | 842 | .text-amber-500 { 843 | --tw-text-opacity: 1; 844 | color: rgb(245 158 11 / var(--tw-text-opacity)); 845 | } 846 | 847 | .text-blue-600 { 848 | --tw-text-opacity: 1; 849 | color: rgb(37 99 235 / var(--tw-text-opacity)); 850 | } 851 | 852 | .text-blue-500 { 853 | --tw-text-opacity: 1; 854 | color: rgb(59 130 246 / var(--tw-text-opacity)); 855 | } 856 | 857 | .text-blue-800 { 858 | --tw-text-opacity: 1; 859 | color: rgb(30 64 175 / var(--tw-text-opacity)); 860 | } 861 | 862 | .text-purple-50 { 863 | --tw-text-opacity: 1; 864 | color: rgb(250 245 255 / var(--tw-text-opacity)); 865 | } 866 | 867 | .file\:mr-4::-webkit-file-upload-button { 868 | margin-right: 1rem; 869 | } 870 | 871 | .file\:mr-4::file-selector-button { 872 | margin-right: 1rem; 873 | } 874 | 875 | .file\:rounded-full::-webkit-file-upload-button { 876 | border-radius: 9999px; 877 | } 878 | 879 | .file\:rounded-full::file-selector-button { 880 | border-radius: 9999px; 881 | } 882 | 883 | .file\:border-0::-webkit-file-upload-button { 884 | border-width: 0px; 885 | } 886 | 887 | .file\:border-0::file-selector-button { 888 | border-width: 0px; 889 | } 890 | 891 | .file\:bg-violet-50::-webkit-file-upload-button { 892 | --tw-bg-opacity: 1; 893 | background-color: rgb(245 243 255 / var(--tw-bg-opacity)); 894 | } 895 | 896 | .file\:bg-violet-50::file-selector-button { 897 | --tw-bg-opacity: 1; 898 | background-color: rgb(245 243 255 / var(--tw-bg-opacity)); 899 | } 900 | 901 | .file\:py-2::-webkit-file-upload-button { 902 | padding-top: 0.5rem; 903 | padding-bottom: 0.5rem; 904 | } 905 | 906 | .file\:py-2::file-selector-button { 907 | padding-top: 0.5rem; 908 | padding-bottom: 0.5rem; 909 | } 910 | 911 | .file\:px-4::-webkit-file-upload-button { 912 | padding-left: 1rem; 913 | padding-right: 1rem; 914 | } 915 | 916 | .file\:px-4::file-selector-button { 917 | padding-left: 1rem; 918 | padding-right: 1rem; 919 | } 920 | 921 | .file\:text-sm::-webkit-file-upload-button { 922 | font-size: 0.875rem; 923 | line-height: 1.25rem; 924 | } 925 | 926 | .file\:text-sm::file-selector-button { 927 | font-size: 0.875rem; 928 | line-height: 1.25rem; 929 | } 930 | 931 | .file\:font-semibold::-webkit-file-upload-button { 932 | font-weight: 600; 933 | } 934 | 935 | .file\:font-semibold::file-selector-button { 936 | font-weight: 600; 937 | } 938 | 939 | .file\:text-violet-700::-webkit-file-upload-button { 940 | --tw-text-opacity: 1; 941 | color: rgb(109 40 217 / var(--tw-text-opacity)); 942 | } 943 | 944 | .file\:text-violet-700::file-selector-button { 945 | --tw-text-opacity: 1; 946 | color: rgb(109 40 217 / var(--tw-text-opacity)); 947 | } 948 | 949 | .hover\:file\:bg-violet-100::-webkit-file-upload-button:hover { 950 | --tw-bg-opacity: 1; 951 | background-color: rgb(237 233 254 / var(--tw-bg-opacity)); 952 | } 953 | 954 | .hover\:file\:bg-violet-100::file-selector-button:hover { 955 | --tw-bg-opacity: 1; 956 | background-color: rgb(237 233 254 / var(--tw-bg-opacity)); 957 | } 958 | 959 | @media (min-width: 640px) { 960 | .sm\:flex-row { 961 | flex-direction: row; 962 | } 963 | 964 | .sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) { 965 | --tw-space-x-reverse: 0; 966 | margin-right: calc(1rem * var(--tw-space-x-reverse)); 967 | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); 968 | } 969 | 970 | .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) { 971 | --tw-space-y-reverse: 0; 972 | margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); 973 | margin-bottom: calc(0px * var(--tw-space-y-reverse)); 974 | } 975 | 976 | .sm\:text-left { 977 | text-align: left; 978 | } 979 | 980 | .sm\:text-3xl { 981 | font-size: 1.875rem; 982 | line-height: 2.25rem; 983 | } 984 | } 985 | --------------------------------------------------------------------------------