nuxt-file-storage 0.2.9 → 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/README.md +19 -10
- package/dist/module.d.mts +9 -1
- package/dist/module.d.ts +9 -1
- package/dist/module.json +5 -1
- package/dist/module.mjs +11 -15
- package/dist/runtime/composables/useFileStorage.d.ts +22 -4
- package/dist/runtime/composables/{useFileStorage.mjs → useFileStorage.js} +6 -2
- 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 +55 -12
- package/dist/runtime/server/utils/storage.js +122 -0
- package/dist/types.d.mts +1 -16
- package/dist/types.d.ts +1 -16
- package/package.json +51 -45
- package/dist/runtime/server/utils/storage.mjs +0 -37
- /package/dist/runtime/plugins/{plugin.mjs → plugin.js} +0 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[![License][license-src]][license-href]
|
|
9
9
|
[![Nuxt][nuxt-src]][nuxt-href]
|
|
10
10
|
|
|
11
|
-
Easy solution to store files in your nuxt apps. Be able to upload files from the frontend and
|
|
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
12
|
|
|
13
13
|
- [✨ Release Notes](/CHANGELOG.md)
|
|
14
14
|
- [🏀 Online playground](https://stackblitz.com/github/NyllRE/nuxt-file-storage?file=playground%2Fapp.vue)
|
|
@@ -128,13 +128,15 @@ You have to create a new instance of `useFileStorage` for each input field
|
|
|
128
128
|
} = useFileStorage() ← | 2 |
|
|
129
129
|
</script>
|
|
130
130
|
```
|
|
131
|
-
by calling a new `useFileStorage` instance you
|
|
131
|
+
by calling a new `useFileStorage` instance you separate the internal logic between the inputs
|
|
132
132
|
|
|
133
133
|
### Handling files in the backend
|
|
134
|
-
using Nitro Server Engine, we will make an api route that
|
|
134
|
+
using Nitro Server Engine, we will make an api route that receives the files and stores them in the folder `userFiles`
|
|
135
135
|
```ts
|
|
136
|
+
import { ServerFile } from "nuxt-file-storage";
|
|
137
|
+
|
|
136
138
|
export default defineEventHandler(async (event) => {
|
|
137
|
-
const { files } = await readBody<{ files:
|
|
139
|
+
const { files } = await readBody<{ files: ServerFile[] }>(event)
|
|
138
140
|
|
|
139
141
|
for ( const file of files ) {
|
|
140
142
|
await storeFileLocally(
|
|
@@ -149,13 +151,20 @@ export default defineEventHandler(async (event) => {
|
|
|
149
151
|
const { binaryString, ext } = parseDataUrl(file.content)
|
|
150
152
|
}
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
// Deleting Files
|
|
155
|
+
await deleteFile('requiredFile.txt', '/userFiles')
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
})
|
|
159
168
|
```
|
|
160
169
|
|
|
161
170
|
And that's it! Now you can store any file in your nuxt project from the user ✨
|
package/dist/module.d.mts
CHANGED
|
@@ -19,6 +19,14 @@ interface ModuleOptions {
|
|
|
19
19
|
version: string
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
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
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
23
31
|
|
|
24
32
|
export { type ClientFile, type ModuleOptions, type ServerFile, _default as default };
|
package/dist/module.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ interface ModuleOptions {
|
|
|
19
19
|
version: string
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
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
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
23
31
|
|
|
24
32
|
export { type ClientFile, type ModuleOptions, type ServerFile, _default as default };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,8 +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
|
-
const version = "0.2.9";
|
|
5
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);
|
|
6
13
|
const module = defineNuxtModule({
|
|
7
14
|
meta: {
|
|
8
15
|
name: "nuxt-file-storage",
|
|
@@ -18,19 +25,8 @@ const module = defineNuxtModule({
|
|
|
18
25
|
config.public.fileStorage = defu(config.public.fileStorage, {
|
|
19
26
|
...options
|
|
20
27
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const previousVersion = config.public.fileStorage?.version;
|
|
24
|
-
if (!previousVersion || previousVersion !== version) {
|
|
25
|
-
logger.warn(
|
|
26
|
-
`There is a breaking change in the \`storeFileLocally\` method, link to changelog:
|
|
27
|
-
https://github.com/NyllRE/nuxt-file-storage/releases/tag/v${version}
|
|
28
|
-
`
|
|
29
|
-
);
|
|
30
|
-
config.public.fileStorage.version = version;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const resolve = createResolver(import.meta.url).resolve;
|
|
28
|
+
logger.ready(`Nuxt File Storage has mounted successfully`);
|
|
29
|
+
const resolve = createResolver(__dirname).resolve;
|
|
34
30
|
addImportsDir(resolve("runtime/composables"));
|
|
35
31
|
addServerScanDir(resolve("./runtime/server"));
|
|
36
32
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClientFile } from '../../types.js';
|
|
1
2
|
type Options = {
|
|
2
3
|
clearOldFiles: boolean;
|
|
3
4
|
};
|
|
@@ -5,18 +6,35 @@ export default function (options?: Options): {
|
|
|
5
6
|
files: import("vue").Ref<{
|
|
6
7
|
content: string | {
|
|
7
8
|
readonly byteLength: number;
|
|
8
|
-
slice: (begin
|
|
9
|
-
readonly [Symbol.toStringTag]:
|
|
9
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
10
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
10
11
|
} | null | undefined;
|
|
11
12
|
name: string;
|
|
12
13
|
lastModified: number;
|
|
13
14
|
readonly size: number;
|
|
14
15
|
readonly type: string;
|
|
15
16
|
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
bytes: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
18
|
+
slice: (start?: number, end?: number, contentType?: string) => Blob;
|
|
19
|
+
stream: () => ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
20
|
+
text: () => Promise<string>;
|
|
21
|
+
}[], ClientFile[] | {
|
|
22
|
+
content: string | {
|
|
23
|
+
readonly byteLength: number;
|
|
24
|
+
slice: (begin?: number, end?: number) => ArrayBuffer;
|
|
25
|
+
readonly [Symbol.toStringTag]: "ArrayBuffer";
|
|
26
|
+
} | null | undefined;
|
|
27
|
+
name: string;
|
|
28
|
+
lastModified: number;
|
|
29
|
+
readonly size: number;
|
|
30
|
+
readonly type: string;
|
|
31
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
32
|
+
bytes: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
33
|
+
slice: (start?: number, end?: number, contentType?: string) => Blob;
|
|
34
|
+
stream: () => ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
18
35
|
text: () => Promise<string>;
|
|
19
36
|
}[]>;
|
|
20
37
|
handleFileInput: (event: any) => Promise<void>;
|
|
38
|
+
clearFiles: () => void;
|
|
21
39
|
};
|
|
22
40
|
export {};
|
|
@@ -21,9 +21,12 @@ export default function(options = { clearOldFiles: true }) {
|
|
|
21
21
|
reader.readAsDataURL(file);
|
|
22
22
|
});
|
|
23
23
|
};
|
|
24
|
+
const clearFiles = () => {
|
|
25
|
+
files.value.splice(0, files.value.length);
|
|
26
|
+
};
|
|
24
27
|
const handleFileInput = async (event) => {
|
|
25
28
|
if (options.clearOldFiles) {
|
|
26
|
-
|
|
29
|
+
clearFiles();
|
|
27
30
|
}
|
|
28
31
|
const promises = [];
|
|
29
32
|
for (const file of event.target.files) {
|
|
@@ -33,6 +36,7 @@ export default function(options = { clearOldFiles: true }) {
|
|
|
33
36
|
};
|
|
34
37
|
return {
|
|
35
38
|
files,
|
|
36
|
-
handleFileInput
|
|
39
|
+
handleFileInput,
|
|
40
|
+
clearFiles
|
|
37
41
|
};
|
|
38
42
|
}
|
|
@@ -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,25 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
import type {
|
|
1
|
+
import type { ServerFile } from '../../../types.js';
|
|
2
|
+
import type { H3Event, EventHandlerRequest } from 'h3';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* @description Will store the file in the specified directory
|
|
5
|
+
* @param file provide the file object
|
|
6
|
+
* @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
|
|
7
|
+
* @param filelocation provide the folder you wish to locate the file in
|
|
5
8
|
* @returns file name: `${filename}`.`${fileExtension}`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* [Documentation](https://github.com/NyllRE/nuxt-file-storage#handling-files-in-the-backend)
|
|
12
|
+
*
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import type { ServerFile } from "nuxt-file-storage";
|
|
17
|
+
*
|
|
18
|
+
* export default defineEventHandler(async (event) => {
|
|
19
|
+
* const { file } = await readBody<{ file: ServerFile }>(event);
|
|
20
|
+
* await storeFileLocally( file, 8, '/userFiles' );
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
9
23
|
*/
|
|
10
24
|
export declare const storeFileLocally: (file: ServerFile, fileNameOrIdLength: string | number, filelocation?: string) => Promise<string>;
|
|
11
25
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @param
|
|
14
|
-
* @param
|
|
26
|
+
* @description Get file path in the specified directory
|
|
27
|
+
* @param filename provide the file name (return of storeFileLocally)
|
|
28
|
+
* @param filelocation provide the folder you wish to locate the file in
|
|
29
|
+
* @returns file path: `${config.fileStorage.mount}/${filelocation}/${filename}`
|
|
30
|
+
*/
|
|
31
|
+
export declare const getFileLocally: (filename: string, filelocation?: string) => string;
|
|
32
|
+
/**
|
|
33
|
+
* @description Get all files in the specified directory
|
|
34
|
+
* @param filelocation provide the folder you wish to locate the file in
|
|
35
|
+
* @returns all files in filelocation: `${config.fileStorage.mount}/${filelocation}`
|
|
36
|
+
*/
|
|
37
|
+
export declare const getFilesLocally: (filelocation?: string) => Promise<string[]>;
|
|
38
|
+
/**
|
|
39
|
+
* @param filename the name of the file you want to delete
|
|
40
|
+
* @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`
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* await deleteFile('/userFiles', 'requiredFile.txt')
|
|
44
|
+
* ```
|
|
15
45
|
*/
|
|
16
46
|
export declare const deleteFile: (filename: string, filelocation?: string) => Promise<void>;
|
|
17
47
|
/**
|
|
18
|
-
Parses a data URL and returns an object with the binary data and the file extension.
|
|
19
|
-
@param {string} file - The data URL
|
|
20
|
-
@returns {{
|
|
48
|
+
* @description Parses a data URL and returns an object with the binary data and the file extension.
|
|
49
|
+
* @param {string} file - The data URL
|
|
50
|
+
* @returns {{binaryString: Buffer, ext: string}} An object with the binary data - file extension
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const { binaryString, ext } = parseDataUrl(file.content)
|
|
55
|
+
* ```
|
|
21
56
|
*/
|
|
22
57
|
export declare const parseDataUrl: (file: string) => {
|
|
23
58
|
binaryString: Buffer;
|
|
24
59
|
ext: string;
|
|
25
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>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { writeFile, rm, mkdir, readdir } from "fs/promises";
|
|
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
|
+
};
|
|
17
|
+
export const storeFileLocally = async (file, fileNameOrIdLength, filelocation = "") => {
|
|
18
|
+
const { binaryString, ext } = parseDataUrl(file.content);
|
|
19
|
+
const location = getMount();
|
|
20
|
+
if (!location)
|
|
21
|
+
throw new Error("fileStorage.mount is not configured");
|
|
22
|
+
const originalExt = file.name.toString().split(".").pop() || ext;
|
|
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, {
|
|
36
|
+
flag: "w"
|
|
37
|
+
});
|
|
38
|
+
return filename;
|
|
39
|
+
};
|
|
40
|
+
export const getFileLocally = (filename, filelocation = "") => {
|
|
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
|
+
});
|
|
56
|
+
};
|
|
57
|
+
export const getFilesLocally = async (filelocation = "") => {
|
|
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(() => []);
|
|
64
|
+
};
|
|
65
|
+
export const deleteFile = async (filename, filelocation = "") => {
|
|
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);
|
|
73
|
+
};
|
|
74
|
+
const generateRandomId = (length) => {
|
|
75
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
76
|
+
let randomId = "";
|
|
77
|
+
for (let i = 0; i < length; i++) {
|
|
78
|
+
randomId += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
79
|
+
}
|
|
80
|
+
return randomId;
|
|
81
|
+
};
|
|
82
|
+
export const parseDataUrl = (file) => {
|
|
83
|
+
const arr = file.split(",");
|
|
84
|
+
const mimeMatch = arr[0].match(/:(.*?);/);
|
|
85
|
+
if (!mimeMatch) {
|
|
86
|
+
throw new Error("Invalid data URL");
|
|
87
|
+
}
|
|
88
|
+
const mime = mimeMatch[1];
|
|
89
|
+
const base64String = arr[1];
|
|
90
|
+
const binaryString = Buffer.from(base64String, "base64");
|
|
91
|
+
const ext = mime.split("/")[1];
|
|
92
|
+
return { binaryString, ext };
|
|
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,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ModuleOptions } from './module.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
declare module '@nuxt/schema' {
|
|
6
|
-
interface NuxtConfig { ['fileStorage']?: Partial<ModuleOptions> }
|
|
7
|
-
interface NuxtOptions { ['fileStorage']?: ModuleOptions }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
declare module 'nuxt/schema' {
|
|
11
|
-
interface NuxtConfig { ['fileStorage']?: Partial<ModuleOptions> }
|
|
12
|
-
interface NuxtOptions { ['fileStorage']?: ModuleOptions }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export type { ClientFile, ModuleOptions, ServerFile, default } from './module.js'
|
|
1
|
+
export { type useRuntimeConfig } from './module.js'
|
package/dist/types.d.ts
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ModuleOptions } from './module'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
declare module '@nuxt/schema' {
|
|
6
|
-
interface NuxtConfig { ['fileStorage']?: Partial<ModuleOptions> }
|
|
7
|
-
interface NuxtOptions { ['fileStorage']?: ModuleOptions }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
declare module 'nuxt/schema' {
|
|
11
|
-
interface NuxtConfig { ['fileStorage']?: Partial<ModuleOptions> }
|
|
12
|
-
interface NuxtOptions { ['fileStorage']?: ModuleOptions }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export type { ClientFile, ModuleOptions, ServerFile, default } from './module'
|
|
1
|
+
export { type useRuntimeConfig } from './module'
|
package/package.json
CHANGED
|
@@ -1,46 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
2
|
+
"name": "nuxt-file-storage",
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
+
"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",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest watch"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@nuxt/kit": "^3.15.4",
|
|
33
|
+
"defu": "^6.1.4"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@nuxt/devtools": "latest",
|
|
37
|
+
"@nuxt/eslint-config": "^0.2.0",
|
|
38
|
+
"@nuxt/module-builder": "^0.8.4",
|
|
39
|
+
"@nuxt/schema": "^3.15.4",
|
|
40
|
+
"@nuxt/test-utils": "^3.21.0",
|
|
41
|
+
"@types/node": "^20.17.19",
|
|
42
|
+
"@vitest/ui": "4.0.16",
|
|
43
|
+
"@vue/test-utils": "^2.4.6",
|
|
44
|
+
"changelogen": "^0.5.7",
|
|
45
|
+
"eslint": "^8.57.1",
|
|
46
|
+
"happy-dom": "^20.0.11",
|
|
47
|
+
"nuxt": "^3.15.4",
|
|
48
|
+
"playwright-core": "^1.57.0",
|
|
49
|
+
"typescript": "^5.9.3",
|
|
50
|
+
"vitest": "^4.0.16"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { writeFile, rm, mkdir } from "fs/promises";
|
|
2
|
-
import { useRuntimeConfig } from "#imports";
|
|
3
|
-
export const storeFileLocally = async (file, fileNameOrIdLength, filelocation = "") => {
|
|
4
|
-
const { binaryString, ext } = parseDataUrl(file.content);
|
|
5
|
-
const location = useRuntimeConfig().public.fileStorage.mount;
|
|
6
|
-
const originalExt = file.name.toString().split(".").pop() || ext;
|
|
7
|
-
const filename = typeof fileNameOrIdLength == "number" ? `${generateRandomId(fileNameOrIdLength)}.${originalExt}` : `${fileNameOrIdLength}.${originalExt}`;
|
|
8
|
-
await mkdir(`${location}${filelocation}`, { recursive: true });
|
|
9
|
-
await writeFile(`${location}${filelocation}/${filename}`, binaryString, {
|
|
10
|
-
flag: "w"
|
|
11
|
-
});
|
|
12
|
-
return filename;
|
|
13
|
-
};
|
|
14
|
-
export const deleteFile = async (filename, filelocation = "") => {
|
|
15
|
-
const location = useRuntimeConfig().public.fileStorage.mount;
|
|
16
|
-
await rm(`${location}${filelocation}/${filename}`);
|
|
17
|
-
};
|
|
18
|
-
const generateRandomId = (length) => {
|
|
19
|
-
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
20
|
-
let randomId = "";
|
|
21
|
-
for (let i = 0; i < length; i++) {
|
|
22
|
-
randomId += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
23
|
-
}
|
|
24
|
-
return randomId;
|
|
25
|
-
};
|
|
26
|
-
export const parseDataUrl = (file) => {
|
|
27
|
-
const arr = file.split(",");
|
|
28
|
-
const mimeMatch = arr[0].match(/:(.*?);/);
|
|
29
|
-
if (!mimeMatch) {
|
|
30
|
-
throw new Error("Invalid data URL");
|
|
31
|
-
}
|
|
32
|
-
const mime = mimeMatch[1];
|
|
33
|
-
const base64String = arr[1];
|
|
34
|
-
const binaryString = Buffer.from(base64String, "base64");
|
|
35
|
-
const ext = mime.split("/")[1];
|
|
36
|
-
return { binaryString, ext };
|
|
37
|
-
};
|
|
File without changes
|