@mad-c/file-system-helpers 1.0.0
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 +504 -0
- package/README.md +3 -0
- package/dist/fs-access.d.ts +14 -0
- package/dist/fs-access.js +103 -0
- package/dist/opfs.d.ts +247 -0
- package/dist/opfs.js +283 -0
- package/dist/path.d.ts +56 -0
- package/dist/path.js +180 -0
- package/dist/permissions.d.ts +14 -0
- package/dist/permissions.js +25 -0
- package/package.json +48 -0
package/dist/path.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a path string into segments and normalize it.
|
|
3
|
+
*
|
|
4
|
+
* The normalization does the following:
|
|
5
|
+
* - Removes empty path segments.
|
|
6
|
+
* - Decode from URI parts to handle paths coming from URLs.
|
|
7
|
+
*
|
|
8
|
+
* @param path The path to split into segments
|
|
9
|
+
*/
|
|
10
|
+
export function pathToSegments(path) {
|
|
11
|
+
const segments = path.split('/').filter(Boolean).map((part) => part.trim());
|
|
12
|
+
return segments;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns the directory name, given a path string.
|
|
16
|
+
* It is similar to node's `dirname` fucntion.
|
|
17
|
+
*
|
|
18
|
+
* @param path The path to get the directory name
|
|
19
|
+
*/
|
|
20
|
+
export function dirname(path) {
|
|
21
|
+
const segments = pathToSegments(path);
|
|
22
|
+
const pathBase = path.startsWith('/') ? '/' : '';
|
|
23
|
+
if (segments.length <= 1) {
|
|
24
|
+
return pathBase;
|
|
25
|
+
}
|
|
26
|
+
const dirSegments = segments.slice(0, -1);
|
|
27
|
+
return `${pathBase}${dirSegments.join('/')}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the name for the last part of a given path string.
|
|
31
|
+
* It is similar to node's `basename` function.
|
|
32
|
+
*
|
|
33
|
+
* @param path The path to get the last part.
|
|
34
|
+
* @param suffix An optional extension to remove from the item, like an extension.
|
|
35
|
+
*/
|
|
36
|
+
export function basename(path, suffix) {
|
|
37
|
+
const segments = pathToSegments(path);
|
|
38
|
+
const lastSegment = segments.at(-1) ?? '';
|
|
39
|
+
if (suffix && lastSegment.endsWith(suffix)) {
|
|
40
|
+
return lastSegment.slice(0, -suffix.length);
|
|
41
|
+
}
|
|
42
|
+
return lastSegment;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns the extension of the path, from the last occurrence of the `.` (dot) to the end of the string.
|
|
46
|
+
* Returns an empty string if there is no dot or if the only dot is in the start of the string.
|
|
47
|
+
* It is similar to node's `extname` function.
|
|
48
|
+
*
|
|
49
|
+
* @param path The path to get the extension
|
|
50
|
+
*/
|
|
51
|
+
export function extname(path) {
|
|
52
|
+
const base = basename(path);
|
|
53
|
+
const index = base.lastIndexOf('.');
|
|
54
|
+
if (index <= 0) {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
return base.slice(index);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves a sequence of paths or path segments into an absolute path.
|
|
61
|
+
* It is similar to node's `resolve` function.
|
|
62
|
+
*
|
|
63
|
+
* **Note**: If no absolute path is provided, the function prepends a `/`. It assumes the starting point is _always_ the file system root.
|
|
64
|
+
*
|
|
65
|
+
* @param paths A sequence of paths or path segments
|
|
66
|
+
*/
|
|
67
|
+
export function resolve(...paths) {
|
|
68
|
+
// INfO: start by concatenating all paths
|
|
69
|
+
let fullPath = '';
|
|
70
|
+
for (const path of paths) {
|
|
71
|
+
if (path.startsWith('/')) {
|
|
72
|
+
fullPath = path;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
fullPath = fullPath ? `${fullPath}/${path}` : path;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// INFO: prepend initial dash
|
|
79
|
+
if (!fullPath.startsWith('/')) {
|
|
80
|
+
fullPath = `/${fullPath}`;
|
|
81
|
+
}
|
|
82
|
+
// INFO: resolve `..`, `.`, and empty segments
|
|
83
|
+
const stack = [];
|
|
84
|
+
const segments = pathToSegments(fullPath);
|
|
85
|
+
for (const segment of segments) {
|
|
86
|
+
if (segment === '..') {
|
|
87
|
+
stack.pop();
|
|
88
|
+
}
|
|
89
|
+
else if (segment !== '.' && segment !== '') {
|
|
90
|
+
stack.push(segment);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return `/${stack.join('/')}`;
|
|
94
|
+
}
|
|
95
|
+
const RESTRICTED_NAMES = [
|
|
96
|
+
'CON',
|
|
97
|
+
'PRN',
|
|
98
|
+
'AUX',
|
|
99
|
+
'NUL',
|
|
100
|
+
'COM1',
|
|
101
|
+
'COM2',
|
|
102
|
+
'COM3',
|
|
103
|
+
'COM4',
|
|
104
|
+
'COM5',
|
|
105
|
+
'COM6',
|
|
106
|
+
'COM7',
|
|
107
|
+
'COM8',
|
|
108
|
+
'COM9',
|
|
109
|
+
'LPT1',
|
|
110
|
+
'LPT2',
|
|
111
|
+
'LPT3',
|
|
112
|
+
'LPT4',
|
|
113
|
+
'LPT5',
|
|
114
|
+
'LPT6',
|
|
115
|
+
'LPT7',
|
|
116
|
+
'LPT8',
|
|
117
|
+
'LPT9'
|
|
118
|
+
];
|
|
119
|
+
const RESTRICTED_CHARACTERS = [
|
|
120
|
+
// INFO: C0 control characters
|
|
121
|
+
// oxlint-disable-next-line no-magic-numbers
|
|
122
|
+
...new Array(32).fill('').map((_, i) => String.fromCharCode(i)),
|
|
123
|
+
// INFO: "del" character
|
|
124
|
+
'\x7f',
|
|
125
|
+
// INFO: C1 control characters
|
|
126
|
+
// oxlint-disable-next-line no-magic-numbers
|
|
127
|
+
...new Array(32).fill('').map((_, i) => String.fromCharCode(128 + i)),
|
|
128
|
+
// INFO: characters not allowed on Windows/Linux/Mac
|
|
129
|
+
...['*', '"', '/', '\\', '>', '<', ':', '|', '?', "'"],
|
|
130
|
+
// INFO: also add "%" to make it easier to decode lone "%" symbols
|
|
131
|
+
'%'
|
|
132
|
+
];
|
|
133
|
+
function defaultEncodeReplacer(segment) {
|
|
134
|
+
const trimmedSegment = segment.trim();
|
|
135
|
+
if (trimmedSegment === '..') {
|
|
136
|
+
return '%2e%2e';
|
|
137
|
+
}
|
|
138
|
+
if (trimmedSegment === '.') {
|
|
139
|
+
return '%2e';
|
|
140
|
+
}
|
|
141
|
+
if (RESTRICTED_NAMES.includes(trimmedSegment)) {
|
|
142
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
143
|
+
return [...trimmedSegment].map((char) => `%${char.charCodeAt(0).toString(16)}`).join('');
|
|
144
|
+
}
|
|
145
|
+
// oxlint-disable-next-line typescript/no-misused-spread
|
|
146
|
+
const codePoints = [...trimmedSegment];
|
|
147
|
+
let normalizedSegment = '';
|
|
148
|
+
for (const codePoint of codePoints) {
|
|
149
|
+
if (RESTRICTED_CHARACTERS.includes(codePoint)) {
|
|
150
|
+
normalizedSegment += `%${codePoint.charCodeAt(0).toString(16)}`;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
normalizedSegment += codePoint;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return normalizedSegment;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Encodes a path string, taking care of restricted characters and names and converting them to percent encoded characters.
|
|
160
|
+
*
|
|
161
|
+
* @param path The path to encode.
|
|
162
|
+
* @param replacer A custom replacer function that will be invoked for each segment of the path.
|
|
163
|
+
*/
|
|
164
|
+
export function encodePath(path, replacer) {
|
|
165
|
+
const segments = pathToSegments(path);
|
|
166
|
+
return segments.map((segment) => (replacer ?? defaultEncodeReplacer)(segment)).join('/');
|
|
167
|
+
}
|
|
168
|
+
function defaultDecodeReplacer(segment) {
|
|
169
|
+
return decodeURIComponent(segment.trim().replaceAll(/%(?![0-9a-fA-F]{2})/giu, '%25'));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Decodes a path string, converting back from perdent encoded characters.
|
|
173
|
+
*
|
|
174
|
+
* @param path The path to decode.
|
|
175
|
+
* @param replacer A custom replacer function that will be invoked for each segment of the path.
|
|
176
|
+
*/
|
|
177
|
+
export function decodePath(path, replacer) {
|
|
178
|
+
const segments = pathToSegments(path);
|
|
179
|
+
return segments.map((segment) => (replacer ?? defaultDecodeReplacer)(segment)).join('/');
|
|
180
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the current permission state for the handle.
|
|
3
|
+
*
|
|
4
|
+
* @param handle The handle to check permissions.
|
|
5
|
+
* @param permissions The permission type to check.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getHandlePermisions(handle: FileSystemHandle, permissions?: FileSystemPermissionMode): Promise<PermissionState>;
|
|
8
|
+
/**
|
|
9
|
+
* Checks and then request for permissions for a given handle.
|
|
10
|
+
*
|
|
11
|
+
* @param handle The handle to request permissions.
|
|
12
|
+
* @param permissions The permission type to get.
|
|
13
|
+
*/
|
|
14
|
+
export declare function requestHandlePermissions(handle: FileSystemHandle, permissions?: FileSystemPermissionMode): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the current permission state for the handle.
|
|
3
|
+
*
|
|
4
|
+
* @param handle The handle to check permissions.
|
|
5
|
+
* @param permissions The permission type to check.
|
|
6
|
+
*/
|
|
7
|
+
export async function getHandlePermisions(handle, permissions = 'read') {
|
|
8
|
+
const permission = await handle.queryPermission({ mode: permissions });
|
|
9
|
+
return permission;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Checks and then request for permissions for a given handle.
|
|
13
|
+
*
|
|
14
|
+
* @param handle The handle to request permissions.
|
|
15
|
+
* @param permissions The permission type to get.
|
|
16
|
+
*/
|
|
17
|
+
export async function requestHandlePermissions(handle, permissions = 'read') {
|
|
18
|
+
const permission = await getHandlePermisions(handle, permissions);
|
|
19
|
+
if (permission !== 'granted') {
|
|
20
|
+
const request = await handle.requestPermission({ mode: permissions });
|
|
21
|
+
if (request !== 'granted') {
|
|
22
|
+
throw new Error('Permission to access the entry was denied');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mad-c/file-system-helpers",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Helpwer for working with the File System API",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"file system",
|
|
7
|
+
"FS",
|
|
8
|
+
"browser file system",
|
|
9
|
+
"origin private file system",
|
|
10
|
+
"OPFS"
|
|
11
|
+
],
|
|
12
|
+
"author": "madcampos",
|
|
13
|
+
"license": "LGPL-2.1-or-later",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./dist/opfs.js",
|
|
20
|
+
"./path": "./dist/path.js",
|
|
21
|
+
"./permissions": "./dist/permissions.js",
|
|
22
|
+
"./access": "./dist/fs-access.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"bootstrap": "playwright install chromium",
|
|
26
|
+
"build": "tsc --project tsconfig.build.json",
|
|
27
|
+
"format": "dprint fmt",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"lint:js": "oxlint --fix",
|
|
30
|
+
"lint": "pnpm run typecheck && pnpm run lint:js",
|
|
31
|
+
"test": "vitest",
|
|
32
|
+
"changelog": "bumpy generate",
|
|
33
|
+
"prepublishOnly": "pnpm run build",
|
|
34
|
+
"bump-version": "bumpy version --commit",
|
|
35
|
+
"postbump-version": "bumpy publish"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/wicg-file-system-access": "^2023.10.7",
|
|
39
|
+
"@varlock/bumpy": "^1.18.0",
|
|
40
|
+
"@vitest/browser-playwright": "^4.1.9",
|
|
41
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
42
|
+
"dprint": "^0.54.0",
|
|
43
|
+
"oxlint": "^1.71.0",
|
|
44
|
+
"oxlint-tsgolint": "^0.23.0",
|
|
45
|
+
"typescript": "^6.0.3",
|
|
46
|
+
"vitest": "^4.1.9"
|
|
47
|
+
}
|
|
48
|
+
}
|