nuxt-file-storage 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +197 -193
- package/dist/module.d.mts +25 -17
- package/dist/module.d.ts +25 -17
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -11
- package/dist/runtime/composables/useFileStorage.d.ts +10 -8
- package/dist/runtime/server/utils/path-safety.d.ts +9 -0
- package/dist/runtime/server/utils/path-safety.js +69 -0
- package/dist/runtime/server/utils/storage.d.ts +14 -6
- package/dist/runtime/server/utils/storage.js +88 -15
- package/dist/types.d.mts +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +10 -4
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
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
|
|
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
21
|
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,193 +1,197 @@
|
|
|
1
|
-

|
|
2
|
-
|
|
3
|
-
# Nuxt File Storage
|
|
4
|
-
|
|
5
|
-
[](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
|
-
<!-- - [📖 Documentation](https://example.com) -->
|
|
16
|
-
|
|
17
|
-
## Features
|
|
18
|
-
|
|
19
|
-
<!-- Highlight some of the features your module provide here -->
|
|
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 `<input>` tag:
|
|
72
|
-
|
|
73
|
-
```html
|
|
74
|
-
<template>
|
|
75
|
-
<input type="file" @input="handleFileInput" />
|
|
76
|
-
</template>
|
|
77
|
-
|
|
78
|
-
<script setup>
|
|
79
|
-
// handleFileInput can handle multiple files
|
|
80
|
-
// clearOldFiles: true by default, each time the user adds files the `files` ref will be cleared
|
|
81
|
-
const { handleFileInput, files } = useFileStorage({ clearOldFiles: false })
|
|
82
|
-
</script>
|
|
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
|
-
<br>
|
|
89
|
-
|
|
90
|
-
Here's an example of using files to send them to the backend
|
|
91
|
-
```html
|
|
92
|
-
<template>
|
|
93
|
-
<input type="file" @input="handleFileInput" />
|
|
94
|
-
<button @click="submit">submit</button>
|
|
95
|
-
</template>
|
|
96
|
-
|
|
97
|
-
<script setup>
|
|
98
|
-
const { handleFileInput, files } = useFileStorage()
|
|
99
|
-
|
|
100
|
-
const submit = async () => {
|
|
101
|
-
const response = await $fetch('/api/files', {
|
|
102
|
-
method: 'POST',
|
|
103
|
-
body: {
|
|
104
|
-
files: files.value
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
</script>
|
|
109
|
-
```
|
|
110
|
-
<br>
|
|
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
|
-
<template>
|
|
118
|
-
<input type="file" @input="handleFileInput" multiple /> ← | 1 |
|
|
119
|
-
<input type="file" @input="profileInputHandler" /> ← | 2 |
|
|
120
|
-
</template>
|
|
121
|
-
|
|
122
|
-
<script setup>
|
|
123
|
-
const { handleFileInput, files } = useFileStorage() ← | 1 |
|
|
124
|
-
|
|
125
|
-
const {
|
|
126
|
-
handleFileInput: profileInputHandler,
|
|
127
|
-
files: profileImage
|
|
128
|
-
} = useFileStorage() ← | 2 |
|
|
129
|
-
</script>
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
[
|
|
191
|
-
[
|
|
192
|
-
[
|
|
193
|
-
[
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Nuxt File Storage
|
|
4
|
+
|
|
5
|
+
[](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
|
+
<!-- - [📖 Documentation](https://example.com) -->
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
<!-- Highlight some of the features your module provide here -->
|
|
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 `<input>` tag:
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<template>
|
|
75
|
+
<input type="file" @input="handleFileInput" />
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup>
|
|
79
|
+
// handleFileInput can handle multiple files
|
|
80
|
+
// clearOldFiles: true by default, each time the user adds files the `files` ref will be cleared
|
|
81
|
+
const { handleFileInput, files } = useFileStorage({ clearOldFiles: false })
|
|
82
|
+
</script>
|
|
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
|
+
<br>
|
|
89
|
+
|
|
90
|
+
Here's an example of using files to send them to the backend
|
|
91
|
+
```html
|
|
92
|
+
<template>
|
|
93
|
+
<input type="file" @input="handleFileInput" />
|
|
94
|
+
<button @click="submit">submit</button>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<script setup>
|
|
98
|
+
const { handleFileInput, files } = useFileStorage()
|
|
99
|
+
|
|
100
|
+
const submit = async () => {
|
|
101
|
+
const response = await $fetch('/api/files', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
body: {
|
|
104
|
+
files: files.value
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
```
|
|
110
|
+
<br>
|
|
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
|
+
<template>
|
|
118
|
+
<input type="file" @input="handleFileInput" multiple /> ← | 1 |
|
|
119
|
+
<input type="file" @input="profileInputHandler" /> ← | 2 |
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<script setup>
|
|
123
|
+
const { handleFileInput, files } = useFileStorage() ← | 1 |
|
|
124
|
+
|
|
125
|
+
const {
|
|
126
|
+
handleFileInput: profileInputHandler,
|
|
127
|
+
files: profileImage
|
|
128
|
+
} = useFileStorage() ← | 2 |
|
|
129
|
+
</script>
|
|
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
|
+
return await getFileLocally('requiredFile.txt', '/userFiles')
|
|
159
|
+
// returns: {AbsolutePath}/userFiles/requiredFile.txt
|
|
160
|
+
|
|
161
|
+
// Return a NodeStream of the file
|
|
162
|
+
// uses getFileLocally internally
|
|
163
|
+
return await retrieveFileLocally(event, 'requiredFile.txt', '/userFiles')
|
|
164
|
+
|
|
165
|
+
// Get all files in a folder
|
|
166
|
+
return await getFilesLocally('/userFiles')
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
And that's it! Now you can store any file in your nuxt project from the user ✨
|
|
171
|
+
|
|
172
|
+
## Contribution
|
|
173
|
+
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.
|
|
174
|
+
|
|
175
|
+
Want to add some feature? PRs are welcome!
|
|
176
|
+
- Clone this repository
|
|
177
|
+
- install the dependencies
|
|
178
|
+
- prepare the project
|
|
179
|
+
- run dev server
|
|
180
|
+
```bash
|
|
181
|
+
git clone https://github.com/NyllRE/nuxt-file-storage && cd nuxt-file-storage
|
|
182
|
+
npm i
|
|
183
|
+
npm run dev:prepare
|
|
184
|
+
npm run dev
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
<!-- Badges -->
|
|
189
|
+
|
|
190
|
+
[npm-version-src]: https://img.shields.io/npm/v/nuxt-file-storage/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
191
|
+
[npm-version-href]: https://npmjs.com/package/nuxt-file-storage
|
|
192
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-file-storage.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
193
|
+
[npm-downloads-href]: https://npmjs.com/package/nuxt-file-storage
|
|
194
|
+
[license-src]: https://img.shields.io/npm/l/nuxt-file-storage.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
195
|
+
[license-href]: https://npmjs.com/package/nuxt-file-storage
|
|
196
|
+
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
|
|
197
|
+
[nuxt-href]: https://nuxt.com/modules/nuxt-file-storage
|
package/dist/module.d.mts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
|
|
3
|
-
interface ServerFile {
|
|
4
|
-
name: string
|
|
5
|
-
content: string
|
|
6
|
-
size: string
|
|
7
|
-
type: string
|
|
8
|
-
lastModified: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface ClientFile extends Blob {
|
|
12
|
-
content: string | ArrayBuffer | null | undefined
|
|
13
|
-
name: string
|
|
14
|
-
lastModified: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ModuleOptions {
|
|
18
|
-
mount: string
|
|
19
|
-
version: string
|
|
3
|
+
interface ServerFile {
|
|
4
|
+
name: string
|
|
5
|
+
content: string
|
|
6
|
+
size: string
|
|
7
|
+
type: string
|
|
8
|
+
lastModified: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ClientFile extends Blob {
|
|
12
|
+
content: string | ArrayBuffer | null | undefined
|
|
13
|
+
name: string
|
|
14
|
+
lastModified: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ModuleOptions {
|
|
18
|
+
mount: string
|
|
19
|
+
version: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @description Augment the '#imports' module to include useRuntimeConfig
|
|
24
|
+
* this is only needed because this package is consumed as a module
|
|
25
|
+
*/
|
|
26
|
+
declare module '#imports' {
|
|
27
|
+
export function useRuntimeConfig(): any
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
package/dist/module.d.ts
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
|
|
3
|
-
interface ServerFile {
|
|
4
|
-
name: string
|
|
5
|
-
content: string
|
|
6
|
-
size: string
|
|
7
|
-
type: string
|
|
8
|
-
lastModified: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface ClientFile extends Blob {
|
|
12
|
-
content: string | ArrayBuffer | null | undefined
|
|
13
|
-
name: string
|
|
14
|
-
lastModified: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ModuleOptions {
|
|
18
|
-
mount: string
|
|
19
|
-
version: string
|
|
3
|
+
interface ServerFile {
|
|
4
|
+
name: string
|
|
5
|
+
content: string
|
|
6
|
+
size: string
|
|
7
|
+
type: string
|
|
8
|
+
lastModified: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ClientFile extends Blob {
|
|
12
|
+
content: string | ArrayBuffer | null | undefined
|
|
13
|
+
name: string
|
|
14
|
+
lastModified: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ModuleOptions {
|
|
18
|
+
mount: string
|
|
19
|
+
version: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @description Augment the '#imports' module to include useRuntimeConfig
|
|
24
|
+
* this is only needed because this package is consumed as a module
|
|
25
|
+
*/
|
|
26
|
+
declare module '#imports' {
|
|
27
|
+
export function useRuntimeConfig(): any
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { defineNuxtModule, logger, createResolver, addImportsDir, addServerScanDir } from '@nuxt/kit';
|
|
2
|
-
import defu from 'defu';
|
|
2
|
+
import { defu } from 'defu';
|
|
3
3
|
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// -- Unbuild CommonJS Shims --
|
|
7
|
+
import __cjs_url__ from 'url';
|
|
8
|
+
import __cjs_path__ from 'path';
|
|
9
|
+
import __cjs_mod__ from 'module';
|
|
10
|
+
const __filename = __cjs_url__.fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = __cjs_path__.dirname(__filename);
|
|
12
|
+
const require = __cjs_mod__.createRequire(import.meta.url);
|
|
4
13
|
const module = defineNuxtModule({
|
|
5
14
|
meta: {
|
|
6
15
|
name: "nuxt-file-storage",
|
|
@@ -16,16 +25,8 @@ const module = defineNuxtModule({
|
|
|
16
25
|
config.public.fileStorage = defu(config.public.fileStorage, {
|
|
17
26
|
...options
|
|
18
27
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"Please provide a mount path for the file storage module in your nuxt.config.js"
|
|
22
|
-
);
|
|
23
|
-
} else {
|
|
24
|
-
logger.ready(
|
|
25
|
-
`Nuxt File Storage has mounted successfully`
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
const resolve = createResolver(import.meta.url).resolve;
|
|
28
|
+
logger.ready(`Nuxt File Storage has mounted successfully`);
|
|
29
|
+
const resolve = createResolver(__dirname).resolve;
|
|
29
30
|
addImportsDir(resolve("runtime/composables"));
|
|
30
31
|
addServerScanDir(resolve("./runtime/server"));
|
|
31
32
|
}
|
|
@@ -6,30 +6,32 @@ export default function (options?: Options): {
|
|
|
6
6
|
files: import("vue").Ref<{
|
|
7
7
|
content: string | {
|
|
8
8
|
readonly byteLength: number;
|
|
9
|
-
slice: (begin
|
|
10
|
-
readonly [Symbol.toStringTag]:
|
|
9
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
10
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
11
11
|
} | null | undefined;
|
|
12
12
|
name: string;
|
|
13
13
|
lastModified: number;
|
|
14
14
|
readonly size: number;
|
|
15
15
|
readonly type: string;
|
|
16
16
|
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
bytes: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
18
|
+
slice: (start?: number, end?: number, contentType?: string) => Blob;
|
|
19
|
+
stream: () => ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
19
20
|
text: () => Promise<string>;
|
|
20
21
|
}[], ClientFile[] | {
|
|
21
22
|
content: string | {
|
|
22
23
|
readonly byteLength: number;
|
|
23
|
-
slice: (begin
|
|
24
|
-
readonly [Symbol.toStringTag]:
|
|
24
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
25
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
25
26
|
} | null | undefined;
|
|
26
27
|
name: string;
|
|
27
28
|
lastModified: number;
|
|
28
29
|
readonly size: number;
|
|
29
30
|
readonly type: string;
|
|
30
31
|
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
bytes: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
33
|
+
slice: (start?: number, end?: number, contentType?: string) => Blob;
|
|
34
|
+
stream: () => ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
33
35
|
text: () => Promise<string>;
|
|
34
36
|
}[]>;
|
|
35
37
|
handleFileInput: (event: any) => Promise<void>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const normalizeRelative: (p: string) => string;
|
|
2
|
+
export declare const isSafeBasename: (name: string) => boolean;
|
|
3
|
+
export declare const ensureSafeBasename: (name: string) => string;
|
|
4
|
+
export declare const containsPathTraversal: (p: string) => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a target path relative to a mount and ensure it cannot escape the mount.
|
|
7
|
+
* Throws on any suspicious input (path traversal, symlink escape, absolute outside mount).
|
|
8
|
+
*/
|
|
9
|
+
export declare const resolveAndEnsureInside: (mount: string, ...parts: string[]) => Promise<string>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { realpath, stat } from "fs/promises";
|
|
3
|
+
import { createError } from "#imports";
|
|
4
|
+
export const normalizeRelative = (p) => {
|
|
5
|
+
if (!p)
|
|
6
|
+
return "";
|
|
7
|
+
return p.replace(/^[/\\]+/, "").replace(/\\/g, "/");
|
|
8
|
+
};
|
|
9
|
+
export const isSafeBasename = (name) => {
|
|
10
|
+
if (!name)
|
|
11
|
+
return false;
|
|
12
|
+
if (name !== path.basename(name))
|
|
13
|
+
return false;
|
|
14
|
+
if (name.includes("\0"))
|
|
15
|
+
return false;
|
|
16
|
+
if (name === "." || name === "..")
|
|
17
|
+
return false;
|
|
18
|
+
if (name.includes("/") || name.includes("\\"))
|
|
19
|
+
return false;
|
|
20
|
+
if (name.split(/[/\\]+/).includes(".."))
|
|
21
|
+
return false;
|
|
22
|
+
return true;
|
|
23
|
+
};
|
|
24
|
+
export const ensureSafeBasename = (name) => {
|
|
25
|
+
if (!isSafeBasename(name))
|
|
26
|
+
throw new Error("Unsafe filename");
|
|
27
|
+
return name;
|
|
28
|
+
};
|
|
29
|
+
export const containsPathTraversal = (p) => {
|
|
30
|
+
if (!p)
|
|
31
|
+
return false;
|
|
32
|
+
if (/^([\\/]|^)?\.\.([\\/]|$)?/.test(p) || /(^|[\\/])\.\.($|[\\/])/.test(p))
|
|
33
|
+
return true;
|
|
34
|
+
const normalized = path.normalize(p);
|
|
35
|
+
const parts = normalized.split(/[/\\]+/);
|
|
36
|
+
return parts.includes("..");
|
|
37
|
+
};
|
|
38
|
+
export const resolveAndEnsureInside = async (mount, ...parts) => {
|
|
39
|
+
if (!mount)
|
|
40
|
+
throw new Error("Mount path must be provided");
|
|
41
|
+
const mountResolved = path.resolve(mount);
|
|
42
|
+
const cleanedParts = parts.map((p) => p.replace(/^[/\\]+/, ""));
|
|
43
|
+
const targetResolved = path.resolve(mountResolved, ...cleanedParts);
|
|
44
|
+
const relative = path.relative(mountResolved, targetResolved);
|
|
45
|
+
if (relative === "" || !relative.startsWith(".." + path.sep) && relative !== "..") {
|
|
46
|
+
let cur = targetResolved;
|
|
47
|
+
while (cur) {
|
|
48
|
+
try {
|
|
49
|
+
await stat(cur);
|
|
50
|
+
break;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const parent = path.dirname(cur);
|
|
53
|
+
if (parent === cur)
|
|
54
|
+
break;
|
|
55
|
+
cur = parent;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const mountReal = await realpath(mountResolved);
|
|
59
|
+
const curReal = await realpath(cur);
|
|
60
|
+
if (!curReal.startsWith(mountReal)) {
|
|
61
|
+
throw new Error("Resolved path escapes configured mount (symlink detected)");
|
|
62
|
+
}
|
|
63
|
+
return targetResolved;
|
|
64
|
+
}
|
|
65
|
+
throw createError({
|
|
66
|
+
statusCode: 400,
|
|
67
|
+
statusMessage: "Resolved path is outside of configured mount"
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
1
|
import type { ServerFile } from '../../../types.js';
|
|
2
|
+
import type { H3Event, EventHandlerRequest } from 'h3';
|
|
4
3
|
/**
|
|
5
4
|
* @description Will store the file in the specified directory
|
|
6
5
|
* @param file provide the file object
|
|
@@ -14,11 +13,12 @@ import type { ServerFile } from '../../../types.js';
|
|
|
14
13
|
*
|
|
15
14
|
* @example
|
|
16
15
|
* ```ts
|
|
17
|
-
* import { ServerFile } from "nuxt-file-storage";
|
|
16
|
+
* import type { ServerFile } from "nuxt-file-storage";
|
|
18
17
|
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
*
|
|
18
|
+
* export default defineEventHandler(async (event) => {
|
|
19
|
+
* const { file } = await readBody<{ file: ServerFile }>(event);
|
|
20
|
+
* await storeFileLocally( file, 8, '/userFiles' );
|
|
21
|
+
* })
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
export declare const storeFileLocally: (file: ServerFile, fileNameOrIdLength: string | number, filelocation?: string) => Promise<string>;
|
|
@@ -58,3 +58,11 @@ export declare const parseDataUrl: (file: string) => {
|
|
|
58
58
|
binaryString: Buffer;
|
|
59
59
|
ext: string;
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Retrieve a file as a readable stream from local storage
|
|
63
|
+
* @param event H3 event to set response headers
|
|
64
|
+
* @param filename name of the file to retrieve
|
|
65
|
+
* @param filelocation folder where the file is located
|
|
66
|
+
* @returns Readable stream of the file
|
|
67
|
+
*/
|
|
68
|
+
export declare const retrieveFileLocally: (event: H3Event<EventHandlerRequest>, filename: string, filelocation?: string) => Promise<NodeJS.ReadableStream>;
|
|
@@ -1,31 +1,75 @@
|
|
|
1
1
|
import { writeFile, rm, mkdir, readdir } from "fs/promises";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import path from "path";
|
|
3
|
+
import {
|
|
4
|
+
normalizeRelative,
|
|
5
|
+
ensureSafeBasename,
|
|
6
|
+
resolveAndEnsureInside
|
|
7
|
+
} from "./path-safety.js";
|
|
8
|
+
import { createError, useRuntimeConfig } from "#imports";
|
|
9
|
+
import { createReadStream, promises as fsPromises } from "fs";
|
|
10
|
+
const getMount = () => {
|
|
11
|
+
try {
|
|
12
|
+
return useRuntimeConfig().public.fileStorage.mount;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
return process.env.FILE_STORAGE_MOUNT || process.env.NUXT_FILE_STORAGE_MOUNT;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
4
17
|
export const storeFileLocally = async (file, fileNameOrIdLength, filelocation = "") => {
|
|
5
18
|
const { binaryString, ext } = parseDataUrl(file.content);
|
|
6
|
-
const location =
|
|
19
|
+
const location = getMount();
|
|
20
|
+
if (!location)
|
|
21
|
+
throw new Error("fileStorage.mount is not configured");
|
|
7
22
|
const originalExt = file.name.toString().split(".").pop() || ext;
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
const safeExt = originalExt.replace(/[^a-zA-Z0-9]/g, "") || ext;
|
|
24
|
+
let filename;
|
|
25
|
+
if (typeof fileNameOrIdLength === "number") {
|
|
26
|
+
filename = `${generateRandomId(fileNameOrIdLength)}.${safeExt}`;
|
|
27
|
+
} else {
|
|
28
|
+
ensureSafeBasename(fileNameOrIdLength);
|
|
29
|
+
filename = `${fileNameOrIdLength}.${safeExt}`;
|
|
30
|
+
}
|
|
31
|
+
const normalizedFilelocation = normalizeRelative(filelocation);
|
|
32
|
+
const dirPath = await resolveAndEnsureInside(location, normalizedFilelocation);
|
|
33
|
+
await mkdir(dirPath, { recursive: true });
|
|
34
|
+
const targetPath = await resolveAndEnsureInside(location, normalizedFilelocation, filename);
|
|
35
|
+
await writeFile(targetPath, binaryString, {
|
|
11
36
|
flag: "w"
|
|
12
37
|
});
|
|
13
38
|
return filename;
|
|
14
39
|
};
|
|
15
40
|
export const getFileLocally = (filename, filelocation = "") => {
|
|
16
|
-
const location =
|
|
17
|
-
|
|
18
|
-
|
|
41
|
+
const location = getMount();
|
|
42
|
+
if (!location)
|
|
43
|
+
throw new Error("fileStorage.mount is not configured");
|
|
44
|
+
ensureSafeBasename(filename);
|
|
45
|
+
const normalizedFilelocation = normalizeRelative(filelocation);
|
|
46
|
+
const resolved = path.resolve(location, normalizedFilelocation, filename);
|
|
47
|
+
const mountResolved = path.resolve(location);
|
|
48
|
+
const relative = path.relative(mountResolved, resolved);
|
|
49
|
+
if (relative === "" || !relative.startsWith(".." + path.sep) && relative !== "..") {
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
throw createError({
|
|
53
|
+
statusCode: 400,
|
|
54
|
+
statusMessage: "Resolved path is outside of configured mount"
|
|
55
|
+
});
|
|
19
56
|
};
|
|
20
57
|
export const getFilesLocally = async (filelocation = "") => {
|
|
21
|
-
const location =
|
|
22
|
-
|
|
23
|
-
|
|
58
|
+
const location = getMount();
|
|
59
|
+
if (!location)
|
|
60
|
+
return [];
|
|
61
|
+
const normalizedFilelocation = normalizeRelative(filelocation);
|
|
62
|
+
const dirPath = await resolveAndEnsureInside(location, normalizedFilelocation);
|
|
63
|
+
return await readdir(dirPath).catch(() => []);
|
|
24
64
|
};
|
|
25
65
|
export const deleteFile = async (filename, filelocation = "") => {
|
|
26
|
-
const location =
|
|
27
|
-
|
|
28
|
-
|
|
66
|
+
const location = getMount();
|
|
67
|
+
if (!location)
|
|
68
|
+
throw new Error("fileStorage.mount is not configured");
|
|
69
|
+
ensureSafeBasename(filename);
|
|
70
|
+
const normalizedFilelocation = normalizeRelative(filelocation);
|
|
71
|
+
const targetPath = await resolveAndEnsureInside(location, normalizedFilelocation, filename);
|
|
72
|
+
await rm(targetPath);
|
|
29
73
|
};
|
|
30
74
|
const generateRandomId = (length) => {
|
|
31
75
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -47,3 +91,32 @@ export const parseDataUrl = (file) => {
|
|
|
47
91
|
const ext = mime.split("/")[1];
|
|
48
92
|
return { binaryString, ext };
|
|
49
93
|
};
|
|
94
|
+
export const retrieveFileLocally = async (event, filename, filelocation = "") => {
|
|
95
|
+
const filePath = getFileLocally(filename, filelocation);
|
|
96
|
+
let stats;
|
|
97
|
+
try {
|
|
98
|
+
stats = await fsPromises.stat(filePath);
|
|
99
|
+
if (!stats.isFile()) {
|
|
100
|
+
throw createError({ statusCode: 404, statusMessage: "Not Found" });
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw createError({ statusCode: 404, statusMessage: "Not Found" });
|
|
104
|
+
}
|
|
105
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
106
|
+
const mimeMap = {
|
|
107
|
+
png: "image/png",
|
|
108
|
+
jpg: "image/jpeg",
|
|
109
|
+
jpeg: "image/jpeg",
|
|
110
|
+
gif: "image/gif",
|
|
111
|
+
svg: "image/svg+xml",
|
|
112
|
+
pdf: "application/pdf",
|
|
113
|
+
txt: "text/plain",
|
|
114
|
+
html: "text/html",
|
|
115
|
+
json: "application/json"
|
|
116
|
+
};
|
|
117
|
+
const contentType = mimeMap[ext] || "application/octet-stream";
|
|
118
|
+
event.node.res.setHeader("Content-Type", contentType);
|
|
119
|
+
event.node.res.setHeader("Content-Length", String(stats.size));
|
|
120
|
+
event.node.res.setHeader("Content-Disposition", `inline; filename="${path.basename(filePath)}"`);
|
|
121
|
+
return createReadStream(filePath);
|
|
122
|
+
};
|
package/dist/types.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { type
|
|
1
|
+
export { type useRuntimeConfig } from './module.js'
|
package/dist/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { type
|
|
1
|
+
export { type useRuntimeConfig } from './module'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-file-storage",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
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
5
|
"repository": "NyllRE/nuxt-file-storage",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"dev": "nuxi dev playground",
|
|
23
23
|
"dev:build": "nuxi build playground",
|
|
24
24
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
25
|
-
"
|
|
25
|
+
"bump": "npm version patch -m \"chore(release): %s\"",
|
|
26
|
+
"release": "npm run lint && npm run bump && npm run prepack && changelogen && git push --follow-tags && npm publish",
|
|
26
27
|
"lint": "eslint .",
|
|
27
28
|
"test": "vitest run",
|
|
28
29
|
"test:watch": "vitest watch"
|
|
@@ -36,11 +37,16 @@
|
|
|
36
37
|
"@nuxt/eslint-config": "^0.2.0",
|
|
37
38
|
"@nuxt/module-builder": "^0.8.4",
|
|
38
39
|
"@nuxt/schema": "^3.15.4",
|
|
39
|
-
"@nuxt/test-utils": "^3.
|
|
40
|
+
"@nuxt/test-utils": "^3.21.0",
|
|
40
41
|
"@types/node": "^20.17.19",
|
|
42
|
+
"@vitest/ui": "4.0.16",
|
|
43
|
+
"@vue/test-utils": "^2.4.6",
|
|
41
44
|
"changelogen": "^0.5.7",
|
|
42
45
|
"eslint": "^8.57.1",
|
|
46
|
+
"happy-dom": "^20.0.11",
|
|
43
47
|
"nuxt": "^3.15.4",
|
|
44
|
-
"
|
|
48
|
+
"playwright-core": "^1.57.0",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"vitest": "^4.0.16"
|
|
45
51
|
}
|
|
46
52
|
}
|