nuxt-file-storage 0.2.9 → 0.3.1-beta.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 +21 -11
- package/dist/module.d.mts +4 -21
- package/dist/module.json +5 -1
- package/dist/module.mjs +26 -19
- package/dist/runtime/composables/useFileStorage.d.ts +2 -15
- 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 +57 -0
- package/dist/runtime/server/utils/storage.d.ts +55 -12
- package/dist/runtime/server/utils/storage.js +142 -0
- package/dist/runtime/types.d.ts +23 -0
- package/dist/runtime/types.js +0 -0
- package/dist/types.d.mts +5 -12
- package/package.json +49 -45
- package/dist/module.cjs +0 -5
- package/dist/module.d.ts +0 -24
- package/dist/runtime/server/utils/storage.mjs +0 -37
- package/dist/types.d.ts +0 -16
- /package/dist/runtime/plugins/{plugin.mjs → plugin.js} +0 -0
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
# Nuxt File Storage
|
|
4
4
|
|
|
5
|
-
[](https://badges.pufler.dev)
|
|
5
|
+
<!-- [](https://badges.pufler.dev) -->
|
|
6
6
|
[![npm version][npm-version-src]][npm-version-href]
|
|
7
7
|
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
8
|
+
[](https://nuxt.care/?search=nuxt-file-storage)
|
|
8
9
|
[![License][license-src]][license-href]
|
|
9
10
|
[![Nuxt][nuxt-src]][nuxt-href]
|
|
10
11
|
|
|
11
|
-
Easy solution to store files in your nuxt apps. Be able to upload files from the frontend and
|
|
12
|
+
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
|
|
|
13
14
|
- [✨ Release Notes](/CHANGELOG.md)
|
|
14
15
|
- [🏀 Online playground](https://stackblitz.com/github/NyllRE/nuxt-file-storage?file=playground%2Fapp.vue)
|
|
@@ -128,13 +129,15 @@ You have to create a new instance of `useFileStorage` for each input field
|
|
|
128
129
|
} = useFileStorage() ← | 2 |
|
|
129
130
|
</script>
|
|
130
131
|
```
|
|
131
|
-
by calling a new `useFileStorage` instance you
|
|
132
|
+
by calling a new `useFileStorage` instance you separate the internal logic between the inputs
|
|
132
133
|
|
|
133
134
|
### Handling files in the backend
|
|
134
|
-
using Nitro Server Engine, we will make an api route that
|
|
135
|
+
using Nitro Server Engine, we will make an api route that receives the files and stores them in the folder `userFiles`
|
|
135
136
|
```ts
|
|
137
|
+
import { ServerFile } from "nuxt-file-storage";
|
|
138
|
+
|
|
136
139
|
export default defineEventHandler(async (event) => {
|
|
137
|
-
const { files } = await readBody<{ files:
|
|
140
|
+
const { files } = await readBody<{ files: ServerFile[] }>(event)
|
|
138
141
|
|
|
139
142
|
for ( const file of files ) {
|
|
140
143
|
await storeFileLocally(
|
|
@@ -149,13 +152,20 @@ export default defineEventHandler(async (event) => {
|
|
|
149
152
|
const { binaryString, ext } = parseDataUrl(file.content)
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
// Deleting Files
|
|
156
|
+
await deleteFile('requiredFile.txt', '/userFiles')
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
// Get file path
|
|
159
|
+
return await getFileLocally('requiredFile.txt', '/userFiles')
|
|
160
|
+
// returns: {AbsolutePath}/userFiles/requiredFile.txt
|
|
161
|
+
|
|
162
|
+
// Return a NodeStream of the file
|
|
163
|
+
// uses getFileLocally internally
|
|
164
|
+
return await retrieveFileLocally(event, 'requiredFile.txt', '/userFiles')
|
|
165
|
+
|
|
166
|
+
// Get all files in a folder
|
|
167
|
+
return await getFilesLocally('/userFiles')
|
|
168
|
+
})
|
|
159
169
|
```
|
|
160
170
|
|
|
161
171
|
And that's it! Now you can store any file in your nuxt project from the user ✨
|
package/dist/module.d.mts
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { ModuleOptions } from '../dist/runtime/types.js';
|
|
3
|
+
export * from '../dist/runtime/types.js';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
name: string
|
|
5
|
-
content: string
|
|
6
|
-
size: string
|
|
7
|
-
type: string
|
|
8
|
-
lastModified: string
|
|
9
|
-
}
|
|
5
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
10
6
|
|
|
11
|
-
|
|
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
|
-
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
|
|
23
|
-
|
|
24
|
-
export { type ClientFile, type ModuleOptions, type ServerFile, _default as default };
|
|
7
|
+
export { _default as default };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { defineNuxtModule,
|
|
2
|
-
import defu from 'defu';
|
|
1
|
+
import { defineNuxtModule, createResolver, logger, addImportsDir, addServerScanDir, addTemplate } from '@nuxt/kit';
|
|
2
|
+
import { defu } from 'defu';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
const module = defineNuxtModule({
|
|
4
|
+
const module$1 = defineNuxtModule({
|
|
7
5
|
meta: {
|
|
8
6
|
name: "nuxt-file-storage",
|
|
9
7
|
configKey: "fileStorage"
|
|
@@ -14,26 +12,35 @@ const module = defineNuxtModule({
|
|
|
14
12
|
// version: '0.0.0',
|
|
15
13
|
// },
|
|
16
14
|
setup(options, nuxt) {
|
|
15
|
+
const { resolve } = createResolver(import.meta.url);
|
|
17
16
|
const config = nuxt.options.runtimeConfig;
|
|
18
17
|
config.public.fileStorage = defu(config.public.fileStorage, {
|
|
19
18
|
...options
|
|
20
19
|
});
|
|
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;
|
|
20
|
+
logger.ready(`Nuxt File Storage has mounted successfully`);
|
|
21
|
+
nuxt.options.alias["#file-storage"] = resolve("./runtime");
|
|
34
22
|
addImportsDir(resolve("runtime/composables"));
|
|
35
23
|
addServerScanDir(resolve("./runtime/server"));
|
|
24
|
+
addTemplate({
|
|
25
|
+
filename: "types/nuxt-file-storage.d.ts",
|
|
26
|
+
getContents: () => [
|
|
27
|
+
"declare module '#file-storage' {",
|
|
28
|
+
` const ServerFile: typeof import('${resolve("./runtime/types")}').ServerFile`,
|
|
29
|
+
` const ClientFile: typeof import('${resolve("./runtime/types")}').ClientFile`,
|
|
30
|
+
"}",
|
|
31
|
+
"",
|
|
32
|
+
"declare global {",
|
|
33
|
+
` type ServerFile = import('${resolve("./runtime/types")}').ServerFile`,
|
|
34
|
+
` type ClientFile = import('${resolve("./runtime/types")}').ClientFile`,
|
|
35
|
+
"}",
|
|
36
|
+
"",
|
|
37
|
+
"export {}"
|
|
38
|
+
].join("\n")
|
|
39
|
+
});
|
|
40
|
+
nuxt.hook("prepare:types", async (options2) => {
|
|
41
|
+
options2.references.push({ path: resolve(nuxt.options.buildDir, "types/nuxt-file-storage.d.ts") });
|
|
42
|
+
});
|
|
36
43
|
}
|
|
37
44
|
});
|
|
38
45
|
|
|
39
|
-
export { module as default };
|
|
46
|
+
export { module$1 as default };
|
|
@@ -2,21 +2,8 @@ type Options = {
|
|
|
2
2
|
clearOldFiles: boolean;
|
|
3
3
|
};
|
|
4
4
|
export default function (options?: Options): {
|
|
5
|
-
files: import("vue").Ref<
|
|
6
|
-
content: string | {
|
|
7
|
-
readonly byteLength: number;
|
|
8
|
-
slice: (begin: number, end?: number | undefined) => ArrayBuffer;
|
|
9
|
-
readonly [Symbol.toStringTag]: string;
|
|
10
|
-
} | null | undefined;
|
|
11
|
-
name: string;
|
|
12
|
-
lastModified: number;
|
|
13
|
-
readonly size: number;
|
|
14
|
-
readonly type: string;
|
|
15
|
-
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
16
|
-
slice: (start?: number | undefined, end?: number | undefined, contentType?: string | undefined) => Blob;
|
|
17
|
-
stream: () => ReadableStream<Uint8Array>;
|
|
18
|
-
text: () => Promise<string>;
|
|
19
|
-
}[]>;
|
|
5
|
+
files: import("vue").Ref<any, any>;
|
|
20
6
|
handleFileInput: (event: any) => Promise<void>;
|
|
7
|
+
clearFiles: () => void;
|
|
21
8
|
};
|
|
22
9
|
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,57 @@
|
|
|
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) return "";
|
|
6
|
+
return p.replace(/^[/\\]+/, "").replace(/\\/g, "/");
|
|
7
|
+
};
|
|
8
|
+
export const isSafeBasename = (name) => {
|
|
9
|
+
if (!name) return false;
|
|
10
|
+
if (name !== path.basename(name)) return false;
|
|
11
|
+
if (name.includes("\0")) return false;
|
|
12
|
+
if (name === "." || name === "..") return false;
|
|
13
|
+
if (name.includes("/") || name.includes("\\")) return false;
|
|
14
|
+
if (name.split(/[/\\]+/).includes("..")) return false;
|
|
15
|
+
return true;
|
|
16
|
+
};
|
|
17
|
+
export const ensureSafeBasename = (name) => {
|
|
18
|
+
if (!isSafeBasename(name)) throw new Error("Unsafe filename");
|
|
19
|
+
return name;
|
|
20
|
+
};
|
|
21
|
+
export const containsPathTraversal = (p) => {
|
|
22
|
+
if (!p) return false;
|
|
23
|
+
if (/^([\\/]|^)?\.\.([\\/]|$)?/.test(p) || /(^|[\\/])\.\.($|[\\/])/.test(p)) return true;
|
|
24
|
+
const normalized = path.normalize(p);
|
|
25
|
+
const parts = normalized.split(/[/\\]+/);
|
|
26
|
+
return parts.includes("..");
|
|
27
|
+
};
|
|
28
|
+
export const resolveAndEnsureInside = async (mount, ...parts) => {
|
|
29
|
+
if (!mount) throw new Error("Mount path must be provided");
|
|
30
|
+
const mountResolved = path.resolve(mount);
|
|
31
|
+
const cleanedParts = parts.map((p) => p.replace(/^[/\\]+/, ""));
|
|
32
|
+
const targetResolved = path.resolve(mountResolved, ...cleanedParts);
|
|
33
|
+
const relative = path.relative(mountResolved, targetResolved);
|
|
34
|
+
if (relative === "" || !relative.startsWith(".." + path.sep) && relative !== "..") {
|
|
35
|
+
let cur = targetResolved;
|
|
36
|
+
while (cur) {
|
|
37
|
+
try {
|
|
38
|
+
await stat(cur);
|
|
39
|
+
break;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const parent = path.dirname(cur);
|
|
42
|
+
if (parent === cur) break;
|
|
43
|
+
cur = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const mountReal = await realpath(mountResolved);
|
|
47
|
+
const curReal = await realpath(cur);
|
|
48
|
+
if (!curReal.startsWith(mountReal)) {
|
|
49
|
+
throw new Error("Resolved path escapes configured mount (symlink detected)");
|
|
50
|
+
}
|
|
51
|
+
return targetResolved;
|
|
52
|
+
}
|
|
53
|
+
throw createError({
|
|
54
|
+
statusCode: 400,
|
|
55
|
+
statusMessage: "Resolved path is outside of configured mount"
|
|
56
|
+
});
|
|
57
|
+
};
|
|
@@ -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,142 @@
|
|
|
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) throw new Error("fileStorage.mount is not configured");
|
|
21
|
+
const nameStr = file.name.toString();
|
|
22
|
+
const originalExt = nameStr.includes(".") ? nameStr.split(".").pop() : ext;
|
|
23
|
+
const safeExt = (originalExt || ext).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
|
+
const extensionFromFileName = fileNameOrIdLength.split(".").pop();
|
|
30
|
+
if (!fileNameOrIdLength.includes(".")) {
|
|
31
|
+
filename = `${fileNameOrIdLength}.${safeExt}`;
|
|
32
|
+
} else if (extensionFromFileName === safeExt) {
|
|
33
|
+
filename = fileNameOrIdLength;
|
|
34
|
+
} else {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[nuxt-file-storage] The provided filename "${fileNameOrIdLength}" does not have the expected extension ".${safeExt}". The correct extension will be appended.`
|
|
37
|
+
);
|
|
38
|
+
filename = `${fileNameOrIdLength.split(".").slice(0, -1).join(".")}.${safeExt}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const normalizedFilelocation = normalizeRelative(filelocation);
|
|
42
|
+
const dirPath = await resolveAndEnsureInside(location, normalizedFilelocation);
|
|
43
|
+
try {
|
|
44
|
+
await mkdir(dirPath, { recursive: true });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err?.code === "EEXIST") {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`[nuxt-file-storage] EEXIST: A file already exists at "${dirPath}" where a directory was expected. This typically happens when a file was accidentally created at a path meant for a folder. Please remove or rename the conflicting file.`
|
|
49
|
+
);
|
|
50
|
+
} else if (err?.code === "ENOTDIR") {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`[nuxt-file-storage] ENOTDIR: Cannot create directory "${dirPath}" because a parent path component is a file, not a directory. Check if any part of the path "${normalizedFilelocation}" exists as a file instead of a folder. Please remove or rename the conflicting file.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
const targetPath = await resolveAndEnsureInside(location, normalizedFilelocation, filename);
|
|
58
|
+
await writeFile(targetPath, binaryString, {
|
|
59
|
+
flag: "w"
|
|
60
|
+
});
|
|
61
|
+
return filename;
|
|
62
|
+
};
|
|
63
|
+
export const getFileLocally = (filename, filelocation = "") => {
|
|
64
|
+
const location = getMount();
|
|
65
|
+
if (!location) throw new Error("fileStorage.mount is not configured");
|
|
66
|
+
ensureSafeBasename(filename);
|
|
67
|
+
const normalizedFilelocation = normalizeRelative(filelocation);
|
|
68
|
+
const resolved = path.resolve(location, normalizedFilelocation, filename);
|
|
69
|
+
const mountResolved = path.resolve(location);
|
|
70
|
+
const relative = path.relative(mountResolved, resolved);
|
|
71
|
+
if (relative === "" || !relative.startsWith(".." + path.sep) && relative !== "..") {
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
throw createError({
|
|
75
|
+
statusCode: 400,
|
|
76
|
+
statusMessage: "Resolved path is outside of configured mount"
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
export const getFilesLocally = async (filelocation = "") => {
|
|
80
|
+
const location = getMount();
|
|
81
|
+
if (!location) return [];
|
|
82
|
+
const normalizedFilelocation = normalizeRelative(filelocation);
|
|
83
|
+
const dirPath = await resolveAndEnsureInside(location, normalizedFilelocation);
|
|
84
|
+
return await readdir(dirPath).catch(() => []);
|
|
85
|
+
};
|
|
86
|
+
export const deleteFile = async (filename, filelocation = "") => {
|
|
87
|
+
const location = getMount();
|
|
88
|
+
if (!location) throw new Error("fileStorage.mount is not configured");
|
|
89
|
+
ensureSafeBasename(filename);
|
|
90
|
+
const normalizedFilelocation = normalizeRelative(filelocation);
|
|
91
|
+
const targetPath = await resolveAndEnsureInside(location, normalizedFilelocation, filename);
|
|
92
|
+
await rm(targetPath);
|
|
93
|
+
};
|
|
94
|
+
const generateRandomId = (length) => {
|
|
95
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
96
|
+
let randomId = "";
|
|
97
|
+
for (let i = 0; i < length; i++) {
|
|
98
|
+
randomId += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
99
|
+
}
|
|
100
|
+
return randomId;
|
|
101
|
+
};
|
|
102
|
+
export const parseDataUrl = (file) => {
|
|
103
|
+
const arr = file.split(",");
|
|
104
|
+
const mimeMatch = arr[0].match(/:(.*?);/);
|
|
105
|
+
if (!mimeMatch) {
|
|
106
|
+
throw new Error("Invalid data URL");
|
|
107
|
+
}
|
|
108
|
+
const mime = mimeMatch[1];
|
|
109
|
+
const base64String = arr[1];
|
|
110
|
+
const binaryString = Buffer.from(base64String, "base64");
|
|
111
|
+
const ext = mime.split("/")[1];
|
|
112
|
+
return { binaryString, ext };
|
|
113
|
+
};
|
|
114
|
+
export const retrieveFileLocally = async (event, filename, filelocation = "") => {
|
|
115
|
+
const filePath = getFileLocally(filename, filelocation);
|
|
116
|
+
let stats;
|
|
117
|
+
try {
|
|
118
|
+
stats = await fsPromises.stat(filePath);
|
|
119
|
+
if (!stats.isFile()) {
|
|
120
|
+
throw createError({ statusCode: 404, statusMessage: "Not Found" });
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw createError({ statusCode: 404, statusMessage: "Not Found" });
|
|
124
|
+
}
|
|
125
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
126
|
+
const mimeMap = {
|
|
127
|
+
png: "image/png",
|
|
128
|
+
jpg: "image/jpeg",
|
|
129
|
+
jpeg: "image/jpeg",
|
|
130
|
+
gif: "image/gif",
|
|
131
|
+
svg: "image/svg+xml",
|
|
132
|
+
pdf: "application/pdf",
|
|
133
|
+
txt: "text/plain",
|
|
134
|
+
html: "text/html",
|
|
135
|
+
json: "application/json"
|
|
136
|
+
};
|
|
137
|
+
const contentType = mimeMap[ext] || "application/octet-stream";
|
|
138
|
+
event.node.res.setHeader("Content-Type", contentType);
|
|
139
|
+
event.node.res.setHeader("Content-Length", String(stats.size));
|
|
140
|
+
event.node.res.setHeader("Content-Disposition", `inline; filename="${path.basename(filePath)}"`);
|
|
141
|
+
return createReadStream(filePath);
|
|
142
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ServerFile {
|
|
2
|
+
name: string;
|
|
3
|
+
content: string;
|
|
4
|
+
size: string;
|
|
5
|
+
type: string;
|
|
6
|
+
lastModified: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ClientFile extends Blob {
|
|
9
|
+
content: string | ArrayBuffer | null | undefined;
|
|
10
|
+
name: string;
|
|
11
|
+
lastModified: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ModuleOptions {
|
|
14
|
+
mount: string;
|
|
15
|
+
version: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @description Augment the '#imports' module to include useRuntimeConfig
|
|
19
|
+
* this is only needed because this package is consumed as a module
|
|
20
|
+
*/
|
|
21
|
+
declare module '#imports' {
|
|
22
|
+
function useRuntimeConfig(): any;
|
|
23
|
+
}
|
|
File without changes
|
package/dist/types.d.mts
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
+
import type { NuxtModule } from '@nuxt/schema'
|
|
1
2
|
|
|
2
|
-
import type {
|
|
3
|
+
import type { default as Module } from './module.mjs'
|
|
3
4
|
|
|
5
|
+
export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
interface NuxtConfig { ['fileStorage']?: Partial<ModuleOptions> }
|
|
7
|
-
interface NuxtOptions { ['fileStorage']?: ModuleOptions }
|
|
8
|
-
}
|
|
7
|
+
export { default } from './module.mjs'
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
interface NuxtConfig { ['fileStorage']?: Partial<ModuleOptions> }
|
|
12
|
-
interface NuxtOptions { ['fileStorage']?: ModuleOptions }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export type { ClientFile, ModuleOptions, ServerFile, default } from './module.js'
|
|
9
|
+
export * from '../dist/runtime/types.js'
|
package/package.json
CHANGED
|
@@ -1,46 +1,50 @@
|
|
|
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-beta.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.mts",
|
|
11
|
+
"import": "./dist/module.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/module.mjs",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"prepack": "nuxt-module-build build",
|
|
20
|
+
"dev": "nuxi dev playground",
|
|
21
|
+
"dev:build": "nuxi build playground",
|
|
22
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
23
|
+
"bump": "npm version patch -m \"chore(release): %s\"",
|
|
24
|
+
"release": "npm run lint && npm run bump && npm run prepack && changelogen && git push --follow-tags && npm publish",
|
|
25
|
+
"lint": "eslint .",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest watch"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@nuxt/kit": "^4.3.0",
|
|
31
|
+
"defu": "^6.1.4"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@nuxt/devtools": "^3.1.1",
|
|
35
|
+
"@nuxt/eslint-config": "^1.13.0",
|
|
36
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
37
|
+
"@nuxt/schema": "^4.3.0",
|
|
38
|
+
"@nuxt/test-utils": "^3.23.0",
|
|
39
|
+
"@types/node": "^20.17.19",
|
|
40
|
+
"@vitest/ui": "4.0.16",
|
|
41
|
+
"@vue/test-utils": "^2.4.6",
|
|
42
|
+
"changelogen": "^0.5.7",
|
|
43
|
+
"eslint": "^8.57.1",
|
|
44
|
+
"happy-dom": "^20.0.11",
|
|
45
|
+
"nuxt": "^4.3.0",
|
|
46
|
+
"playwright-core": "^1.57.0",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"vitest": "^4.0.16"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/module.cjs
DELETED
package/dist/module.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import * as _nuxt_schema from '@nuxt/schema';
|
|
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
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
|
|
23
|
-
|
|
24
|
-
export { type ClientFile, type ModuleOptions, type ServerFile, _default as default };
|
|
@@ -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
|
-
};
|
package/dist/types.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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'
|
|
File without changes
|