├── .eslintignore ├── .npmrc ├── playground ├── .env.example ├── tsconfig.json ├── server │ ├── tsconfig.json │ └── api │ │ └── files.ts ├── public │ ├── specificFolder │ │ └── JEAdxnaN15ef.png │ ├── nuxt-storage-logo.svg │ └── nuxt-file-storage-banner.svg ├── nuxt.config.ts ├── package.json └── app.vue ├── tsconfig.json ├── bun.lockb ├── test ├── fixtures │ └── basic │ │ ├── package.json │ │ ├── nuxt.config.ts │ │ ├── app.vue │ │ └── server │ │ └── api │ │ └── files.ts └── basic.test.ts ├── .vscode ├── settings.json └── launch.json ├── src ├── runtime │ ├── plugins │ │ └── plugin.ts │ ├── composables │ │ └── useFileStorage.ts │ └── server │ │ └── utils │ │ └── storage.ts ├── types.d.ts └── module.ts ├── .editorconfig ├── .eslintrc ├── .prettierrc ├── .github ├── FUNDING.yml └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /playground/.env.example: -------------------------------------------------------------------------------- 1 | mount="" # Provide a value for mount 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NyllRE/nuxt-file-storage/HEAD/bun.lockb -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /playground/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "basic", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | ssr: true, 3 | modules: ['../../../src/module'], 4 | }) 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "eslint.validate": ["javascript"] 6 | } 7 | -------------------------------------------------------------------------------- /playground/public/specificFolder/JEAdxnaN15ef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NyllRE/nuxt-file-storage/HEAD/playground/public/specificFolder/JEAdxnaN15ef.png -------------------------------------------------------------------------------- /src/runtime/plugins/plugin.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin } from '#app' 2 | 3 | export default defineNuxtPlugin((nuxtApp) => { 4 | console.log('nuxt-file-storage initialized successfully') 5 | }) 6 | -------------------------------------------------------------------------------- /test/fixtures/basic/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | modules: ['../src/module'], 3 | 4 | fileStorage: { 5 | mount: process.env.mount, 6 | }, 7 | 8 | devtools: { enabled: true }, 9 | compatibilityDate: '2025-02-27', 10 | }) 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 3 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "nuxt-file-storage-playground", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate" 9 | }, 10 | "devDependencies": { 11 | "nuxt": "latest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@nuxt/eslint-config"], 4 | "rules": { 5 | "vue/multi-word-component-names": 0, 6 | "vue/html-indent": 0, 7 | "vue/html-self-closing": 0, 8 | "vue/max-attributes-per-line": 0, 9 | "vue/singleline-html-element-content-newline": 0, 10 | "@typescript-eslint/no-unused-vars": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 3, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "printWidth": 100, 8 | "overrides": [ 9 | { 10 | "files": "*.vue", 11 | "options": { 12 | "singleQuote": true, 13 | "maxAttributesPerLine": 2, 14 | "singleTagSingleLine": false 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /playground/server/api/files.ts: -------------------------------------------------------------------------------- 1 | import type { ServerFile } from '../../../src/types' 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { files } = await readBody<{ files: ServerFile[] }>(event) 5 | const fileNames: string[] = [] 6 | for (const file of files) { 7 | fileNames.push(await storeFileLocally(file, 12, '/specificFolder')) 8 | } 9 | return fileNames 10 | }) 11 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface ServerFile { 2 | name: string 3 | content: string 4 | size: string 5 | type: string 6 | lastModified: string 7 | } 8 | 9 | export interface ClientFile extends Blob { 10 | content: string | ArrayBuffer | null | undefined 11 | name: string 12 | lastModified: number 13 | } 14 | 15 | export interface ModuleOptions { 16 | mount: string 17 | version: string 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/basic/server/api/files.ts: -------------------------------------------------------------------------------- 1 | import { storeFileLocally } from '../../../../../src/runtime/server/utils/storage' 2 | 3 | export default defineEventHandler(async (event) => { 4 | const body = await readBody<{ files: File[] }>(event) 5 | console.dir(body) 6 | 7 | const fileNames: string[] = [] 8 | for (const file of body.files) { 9 | fileNames.push(await storeFileLocally(file.content, file.name)) 10 | } 11 | return fileNames 12 | }) 13 | 14 | interface File { 15 | name: string 16 | content: string 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/dist/module.cjs", 15 | "outFiles": [ 16 | "${workspaceFolder}/**/*.js" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [nyllre] 4 | patreon: nyll 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 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .data 23 | .vercel_build_output 24 | .build-* 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | !.vscode/*.code-snippets 43 | 44 | # Intellij idea 45 | *.iml 46 | .idea 47 | 48 | # OSX 49 | .DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | 58 | playground/public/userFiles/* -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 20 18 | - run: npm i 19 | - run: npm ci 20 | - run: npm test 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: 20 30 | registry-url: https://registry.npmjs.org/ 31 | - run: npm ci 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-Present Nuxt File Storage Project 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. -------------------------------------------------------------------------------- /test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { fileURLToPath } from 'node:url' 3 | import { setup, $fetch } from '@nuxt/test-utils/e2e' 4 | 5 | describe('ssr', async () => { 6 | await setup({ 7 | rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), 8 | }) 9 | 10 | it('renders the index page', async () => { 11 | // Get response to a server-rendered page with `$fetch`. 12 | const html = await $fetch('/') 13 | expect(html).toContain('') 14 | }) 15 | }) 16 | 17 | //! Look into this: https://runthatline.com/how-to-mock-fetch-api-with-vitest/ 18 | // describe('Server API', async () => { 19 | // await setup({ 20 | // rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), 21 | // }) 22 | 23 | // it('sends a file to the server', async () => { 24 | // const fileName = await fetch('/api/files', { 25 | // method: 'POST', 26 | // body: JSON.stringify({ 27 | // files: [ 28 | // { 29 | // name: 'ExampleFile', 30 | // content: 'data:text/plain;base64,dGhpcyBpcyBhbiBleGFtcGxlIGZpbGUK', 31 | // }, 32 | // ], 33 | // }), 34 | // }) 35 | // console.log(`API Request Result: ${fileName}`) 36 | 37 | // expect(fileName).toContain('ExampleFile') 38 | // }) 39 | // }) 40 | -------------------------------------------------------------------------------- /src/runtime/composables/useFileStorage.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import type { ClientFile } from '../../types' 3 | 4 | type Options = { 5 | clearOldFiles: boolean 6 | } 7 | 8 | export default function (options: Options = { clearOldFiles: true }) { 9 | const files = ref([]) 10 | const serializeFile = (file: ClientFile): Promise => { 11 | return new Promise((resolve, reject) => { 12 | const reader = new FileReader() 13 | reader.onload = (e: ProgressEvent) => { 14 | files.value.push({ 15 | ...file, 16 | name: file.name, 17 | size: file.size, 18 | type: file.type, 19 | lastModified: file.lastModified, 20 | content: e.target?.result, 21 | }) 22 | resolve() 23 | } 24 | reader.onerror = (error) => { 25 | reject(error) 26 | } 27 | reader.readAsDataURL(file) 28 | }) 29 | } 30 | 31 | const clearFiles = () => { 32 | files.value.splice(0, files.value.length) 33 | } 34 | 35 | 36 | const handleFileInput = async (event: any) => { 37 | if (options.clearOldFiles) { 38 | clearFiles() 39 | } 40 | 41 | const promises = [] 42 | for (const file of event.target.files) { 43 | promises.push(serializeFile(file)) 44 | } 45 | 46 | await Promise.all(promises) 47 | } 48 | 49 | return { 50 | files, 51 | handleFileInput, 52 | clearFiles, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-file-storage", 3 | "version": "0.3.0", 4 | "description": "Easy solution to store files in your nuxt apps. Be able to upload files from the frontend and recieve them from the backend to then save the files in your project.", 5 | "repository": "NyllRE/nuxt-file-storage", 6 | "license": "MIT", 7 | "type": "module", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/types.d.ts", 11 | "import": "./dist/module.mjs", 12 | "require": "./dist/module.cjs" 13 | } 14 | }, 15 | "main": "./dist/module.cjs", 16 | "types": "./dist/types.d.ts", 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "prepack": "nuxt-module-build build", 22 | "dev": "nuxi dev playground", 23 | "dev:build": "nuxi build playground", 24 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground", 25 | "release": "npm run lint && npm run prepack && changelogen && npm publish && git push --follow-tags", 26 | "lint": "eslint .", 27 | "test": "vitest run", 28 | "test:watch": "vitest watch" 29 | }, 30 | "dependencies": { 31 | "@nuxt/kit": "^3.15.4", 32 | "defu": "^6.1.4" 33 | }, 34 | "devDependencies": { 35 | "@nuxt/devtools": "latest", 36 | "@nuxt/eslint-config": "^0.2.0", 37 | "@nuxt/module-builder": "^0.8.4", 38 | "@nuxt/schema": "^3.15.4", 39 | "@nuxt/test-utils": "^3.17.0", 40 | "@types/node": "^20.17.19", 41 | "changelogen": "^0.5.7", 42 | "eslint": "^8.57.1", 43 | "nuxt": "^3.15.4", 44 | "vitest": "^1.6.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineNuxtModule, 3 | createResolver, 4 | addImportsDir, 5 | addServerScanDir, 6 | logger, 7 | } from '@nuxt/kit' 8 | // import { $fetch } from 'ofetch' 9 | import defu from 'defu' 10 | // import { version } from '../package.json' 11 | 12 | import type { ModuleOptions } from './types' 13 | export type * from './types' 14 | 15 | export default defineNuxtModule({ 16 | meta: { 17 | name: 'nuxt-file-storage', 18 | configKey: 'fileStorage', 19 | }, 20 | //? Default configuration options of the Nuxt module 21 | //! no defaults for now 22 | // defaults: { 23 | // version: '0.0.0', 24 | // }, 25 | setup(options, nuxt) { 26 | const config = nuxt.options.runtimeConfig as any 27 | config.public.fileStorage = defu(config.public.fileStorage, { 28 | ...options, 29 | }) 30 | 31 | if (!config.public.fileStorage.mount) { 32 | logger.error( 33 | 'Please provide a mount path for the file storage module in your nuxt.config.js', 34 | ) 35 | } else { 36 | logger.ready( 37 | `Nuxt File Storage has mounted successfully`, 38 | ) 39 | } 40 | 41 | // if (nuxt.options.dev) { 42 | // // $fetch('https://registry.npmjs.org/nuxt-file-storage/latest') 43 | // // .then((release: any) => { 44 | // // if (release.version > version) 45 | // // logger.info( 46 | // // `A new version of Nuxt File Storage (v${release.version}) is available: https://github.com/nyllre/nuxt-file-storage/releases/latest`, 47 | // // ) 48 | // // }) 49 | // // .catch(() => {}) 50 | // } 51 | 52 | const resolve = createResolver(import.meta.url).resolve 53 | 54 | addImportsDir(resolve('runtime/composables')) 55 | addServerScanDir(resolve('./runtime/server')) 56 | }, 57 | }) 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## v0.3.1 5 | 6 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.3.0...v0.3.1) 7 | 8 | ## v0.2.9 9 | 10 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.2.8...v0.2.9) 11 | 12 | ## v0.2.8 13 | 14 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.2.7...v0.2.8) 15 | 16 | ### 🩹 Fixes 17 | 18 | - Update to latest `@nuxt/module-builder` ([2d0c20c](https://github.com/NyllRE/nuxt-file-storage/commit/2d0c20c)) 19 | 20 | ### ❤️ Contributors 21 | 22 | - Daniel Roe ([@danielroe](http://github.com/danielroe)) 23 | 24 | ## v0.2.7 25 | 26 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.2.6...v0.2.7) 27 | 28 | ## v0.2.6 29 | 30 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.2.5...v0.2.6) 31 | 32 | ### 🏡 Chore 33 | 34 | - **release:** V0.2.3 ([fc8cffa](https://github.com/NyllRE/nuxt-file-storage/commit/fc8cffa)) 35 | 36 | ### ❤️ Contributors 37 | 38 | - NyllRE 39 | 40 | ## v0.2.5 41 | 42 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.2.4...v0.2.5) 43 | 44 | ## v0.2.2 45 | 46 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.2.1...v0.2.2) 47 | 48 | ## v0.2.1 49 | 50 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.1.4...v0.2.1) 51 | 52 | ## v0.1.4 53 | 54 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.1.3...v0.1.4) 55 | 56 | ## v0.1.3 57 | 58 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.1.2...v0.1.3) 59 | 60 | ## v0.1.2 61 | 62 | [compare changes](https://github.com/NyllRE/nuxt-file-storage/compare/v0.1.1...v0.1.2) 63 | 64 | ## v0.1.1 65 | 66 | -------------------------------------------------------------------------------- /playground/public/nuxt-storage-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/runtime/server/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, rm, mkdir, readdir } from 'fs/promises' 2 | import { useRuntimeConfig } from '#imports' 3 | import type { ServerFile } from '../../../types' 4 | import { join } from 'path' 5 | 6 | /** 7 | * @description Will store the file in the specified directory 8 | * @param file provide the file object 9 | * @param fileNameOrIdLength you can pass a string or a number, if you enter a string it will be the file name, if you enter a number it will generate a unique ID 10 | * @param filelocation provide the folder you wish to locate the file in 11 | * @returns file name: `${filename}`.`${fileExtension}` 12 | * 13 | * 14 | * [Documentation](https://github.com/NyllRE/nuxt-file-storage#handling-files-in-the-backend) 15 | * 16 | * 17 | * @example 18 | * ```ts 19 | * import { ServerFile } from "nuxt-file-storage"; 20 | * 21 | * const { file } = await readBody<{ files: ServerFile }>(event) 22 | 23 | * await storeFileLocally( file, 8, '/userFiles' ) 24 | * ``` 25 | */ 26 | export const storeFileLocally = async ( 27 | file: ServerFile, 28 | fileNameOrIdLength: string | number, 29 | filelocation: string = '', 30 | ): Promise => { 31 | const { binaryString, ext } = parseDataUrl(file.content) 32 | const location = useRuntimeConfig().public.fileStorage.mount 33 | 34 | //? Extract the file extension from the original filename 35 | const originalExt = file.name.toString().split('.').pop() || ext 36 | 37 | const filename = 38 | typeof fileNameOrIdLength == 'number' 39 | ? `${generateRandomId(fileNameOrIdLength)}.${originalExt}` 40 | : `${fileNameOrIdLength}.${originalExt}` 41 | 42 | await mkdir(join(location, filelocation), { recursive: true }) 43 | 44 | await writeFile(join(location, filelocation, filename), binaryString as any, { 45 | flag: 'w', 46 | }) 47 | 48 | return filename 49 | } 50 | 51 | /** 52 | * @description Get file path in the specified directory 53 | * @param filename provide the file name (return of storeFileLocally) 54 | * @param filelocation provide the folder you wish to locate the file in 55 | * @returns file path: `${config.fileStorage.mount}/${filelocation}/${filename}` 56 | */ 57 | export const getFileLocally = (filename: string, filelocation: string = ''): string => { 58 | const location = useRuntimeConfig().public.fileStorage.mount 59 | const normalizedFilelocation = filelocation.startsWith('/') ? filelocation.slice(1) : filelocation; 60 | return join(location, normalizedFilelocation, filename) 61 | } 62 | 63 | 64 | /** 65 | * @description Get all files in the specified directory 66 | * @param filelocation provide the folder you wish to locate the file in 67 | * @returns all files in filelocation: `${config.fileStorage.mount}/${filelocation}` 68 | */ 69 | export const getFilesLocally = async (filelocation: string = ''): Promise => { 70 | const location = useRuntimeConfig().public.fileStorage.mount 71 | const normalizedFilelocation = filelocation.startsWith('/') ? filelocation.slice(1) : filelocation; 72 | return await readdir(join(location, normalizedFilelocation)).catch(() => []) 73 | } 74 | 75 | 76 | /** 77 | * @param filename the name of the file you want to delete 78 | * @param filelocation the folder where the file is located, if it is in the root folder you can leave it empty, if it is in a subfolder you can pass the name of the subfolder with a preceding slash: `/subfolder` 79 | * @example 80 | * ```ts 81 | * await deleteFile('/userFiles', 'requiredFile.txt') 82 | * ``` 83 | */ 84 | export const deleteFile = async (filename: string, filelocation: string = '') => { 85 | const location = useRuntimeConfig().public.fileStorage.mount 86 | const normalizedFilelocation = filelocation.startsWith('/') ? filelocation.slice(1) : filelocation; 87 | await rm(join(location, normalizedFilelocation, filename)) 88 | } 89 | 90 | 91 | /** 92 | * @description generates a random ID with the specified length 93 | * @param length the length of the random ID 94 | * @returns a random ID with the specified length 95 | */ 96 | const generateRandomId = (length: number) => { 97 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 98 | let randomId = '' 99 | for (let i = 0; i < length; i++) { 100 | randomId += characters.charAt(Math.floor(Math.random() * characters.length)) 101 | } 102 | return randomId 103 | } 104 | 105 | /** 106 | * @description Parses a data URL and returns an object with the binary data and the file extension. 107 | * @param {string} file - The data URL 108 | * @returns {{binaryString: Buffer, ext: string}} An object with the binary data - file extension 109 | * 110 | * @example 111 | * ```ts 112 | * const { binaryString, ext } = parseDataUrl(file.content) 113 | * ``` 114 | */ 115 | export const parseDataUrl = (file: string): 116 | {binaryString: Buffer, ext: string} => { 117 | const arr: string[] = file.split(',') 118 | const mimeMatch = arr[0].match(/:(.*?);/) 119 | if (!mimeMatch) { 120 | throw new Error('Invalid data URL') 121 | } 122 | const mime: string = mimeMatch[1] 123 | const base64String: string = arr[1] 124 | const binaryString: Buffer = Buffer.from(base64String, 'base64') 125 | 126 | const ext = mime.split('/')[1] 127 | 128 | return { binaryString, ext } 129 | } 130 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 78 | 79 | 80 | 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Nuxt File Storage Banner](./playground/public/nuxt-file-storage-banner.svg) 2 | 3 | # Nuxt File Storage 4 | 5 | [![Visits Badge](https://badges.pufler.dev/visits/nyllre/nuxt-file-storage)](https://badges.pufler.dev) 6 | [![npm version][npm-version-src]][npm-version-href] 7 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 8 | [![License][license-src]][license-href] 9 | [![Nuxt][nuxt-src]][nuxt-href] 10 | 11 | Easy solution to store files in your nuxt apps. Be able to upload files from the frontend and receive them from the backend to then save the files in your project. 12 | 13 | - [✨  Release Notes](/CHANGELOG.md) 14 | - [🏀 Online playground](https://stackblitz.com/github/NyllRE/nuxt-file-storage?file=playground%2Fapp.vue) 15 | 16 | 17 | ## Features 18 | 19 | 20 | 21 | - 📁  Get files from file input and make them ready to send to backend 22 | - ⚗️  Serialize files in the backend to be able to use them appropriately 23 | - 🖴  Store files in a specified location in your Nuxt backend with Nitro Engine 24 | 25 | ## Quick Setup 26 | 27 | 1. Add `nuxt-file-storage` dependency to your project 28 | 29 | ```bash 30 | # Using pnpm 31 | pnpm add -D nuxt-file-storage 32 | 33 | # Using yarn 34 | yarn add --dev nuxt-file-storage 35 | 36 | # Using npm 37 | npm install --save-dev nuxt-file-storage 38 | ``` 39 | 40 | 2. Add `nuxt-file-storage` to the `modules` section of `nuxt.config.ts` 41 | 42 | ```js 43 | export default defineNuxtConfig({ 44 | modules: ['nuxt-file-storage'], 45 | }) 46 | ``` 47 | 48 | That's it! You can now use Nuxt Storage in your Nuxt app ✨ 49 | 50 | ## Configuration 51 | 52 | You can currently configure a single setting of the `nuxt-file-storage` module. Here is the config interface: 53 | 54 | ```js 55 | export default defineNuxtConfig({ 56 | modules: ['nuxt-file-storage'], 57 | fileStorage: { 58 | // enter the absolute path to the location of your storage 59 | mount: '/home/$USR/development/nuxt-file-storage/server/files', 60 | 61 | // {OR} use environment variables (recommended) 62 | mount: process.env.mount 63 | // you need to set the mount in your .env file at the root of your project 64 | }, 65 | }) 66 | ``` 67 | 68 | ## Usage 69 | 70 | ### Handling Files in the frontend 71 | You can use Nuxt Storage to get the files from the `` tag: 72 | 73 | ```html 74 | 77 | 78 | 83 | ``` 84 | The `files` return a ref object that contains the files 85 | 86 | > `handleFileInput` returns a promise in case you need to check if the file input has concluded 87 | 88 |
89 | 90 | Here's an example of using files to send them to the backend 91 | ```html 92 | 96 | 97 | 109 | ``` 110 |
111 | 112 | #### Handling multiple file input fields 113 | You have to create a new instance of `useFileStorage` for each input field 114 | 115 | 116 | ```html 117 | 121 | 122 | 130 | ``` 131 | by calling a new `useFileStorage` instance you separate the internal logic between the inputs 132 | 133 | ### Handling files in the backend 134 | using Nitro Server Engine, we will make an api route that receives the files and stores them in the folder `userFiles` 135 | ```ts 136 | import { ServerFile } from "nuxt-file-storage"; 137 | 138 | export default defineEventHandler(async (event) => { 139 | const { files } = await readBody<{ files: ServerFile[] }>(event) 140 | 141 | for ( const file of files ) { 142 | await storeFileLocally( 143 | file, // the file object 144 | 8, // you can add a name for the file or length of Unique ID that will be automatically generated! 145 | '/userFiles' // the folder the file will be stored in 146 | ) 147 | 148 | // {OR} 149 | 150 | // Parses a data URL and returns an object with the binary data and the file extension. 151 | const { binaryString, ext } = parseDataUrl(file.content) 152 | } 153 | 154 | // Deleting Files 155 | await deleteFile('requiredFile.txt', '/userFiles') 156 | 157 | // Get file path 158 | await getFileLocally('requiredFile.txt', '/userFiles') 159 | // returns: {AbsolutePath}/userFiles/requiredFile.txt 160 | 161 | // Get all files in a folder 162 | await getFilesLocally('/userFiles') 163 | }) 164 | ``` 165 | 166 | And that's it! Now you can store any file in your nuxt project from the user ✨ 167 | 168 | ## Contribution 169 | Run into a problem? Open a [new issue](https://github.com/NyllRE/nuxt-file-storage/issues/new). I'll try my best to include all the features requested if it is fitting to the scope of the project. 170 | 171 | Want to add some feature? PRs are welcome! 172 | - Clone this repository 173 | - install the dependencies 174 | - prepare the project 175 | - run dev server 176 | ```bash 177 | git clone https://github.com/NyllRE/nuxt-file-storage && cd nuxt-file-storage 178 | npm i 179 | npm run dev:prepare 180 | npm run dev 181 | ``` 182 | 183 | 184 | 185 | 186 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-file-storage/latest.svg?style=flat&colorA=18181B&colorB=28CF8D 187 | [npm-version-href]: https://npmjs.com/package/nuxt-file-storage 188 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-file-storage.svg?style=flat&colorA=18181B&colorB=28CF8D 189 | [npm-downloads-href]: https://npmjs.com/package/nuxt-file-storage 190 | [license-src]: https://img.shields.io/npm/l/nuxt-file-storage.svg?style=flat&colorA=18181B&colorB=28CF8D 191 | [license-href]: https://npmjs.com/package/nuxt-file-storage 192 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js 193 | [nuxt-href]: https://nuxt.com/modules/nuxt-file-storage 194 | -------------------------------------------------------------------------------- /playground/public/nuxt-file-storage-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------