just-bash-dropbox 0.1.0 → 0.2.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/README.md +23 -4
- package/dist/dropbox-fs.d.ts +10 -21
- package/dist/dropbox-fs.js +82 -11
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/type-check.d.ts +1 -0
- package/dist/type-check.js +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -107,21 +107,37 @@ rm -r /path/dir # delete directory
|
|
|
107
107
|
mkdir -p /path/deep/nested # create nested directories
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Semantic notes
|
|
111
|
+
|
|
112
|
+
- `lstat` delegates to `stat` (Dropbox has no symlinks)
|
|
113
|
+
- `realpath` verifies the path exists and returns Dropbox's canonical casing
|
|
114
|
+
- `rm /dir` (without `-r`) throws `EISDIR` — matches POSIX behavior
|
|
115
|
+
- `>>` (append) uses optimistic locking via Dropbox's `rev` field to detect concurrent writes
|
|
116
|
+
|
|
110
117
|
### Not supported
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
These operations throw `ENOSYS`:
|
|
113
120
|
|
|
114
121
|
- `chmod` — no file permissions
|
|
115
122
|
- `ln` / `ln -s` — no hard/symbolic links
|
|
116
|
-
- `readlink
|
|
123
|
+
- `readlink` — no symlinks
|
|
124
|
+
- `utimes` — Dropbox manages timestamps
|
|
125
|
+
|
|
126
|
+
### Glob support
|
|
127
|
+
|
|
128
|
+
`getAllPaths()` returns `[]` by default (sync method can't make API calls). For glob support, call `prefetchAllPaths()` first:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
await fs.prefetchAllPaths(); // recursive listing, cached
|
|
132
|
+
fs.getAllPaths(); // returns all paths
|
|
133
|
+
```
|
|
117
134
|
|
|
118
135
|
### Limitations
|
|
119
136
|
|
|
120
137
|
- **File size**: Single uploads are limited to 150 MB
|
|
121
|
-
- **Append**: `>>`
|
|
138
|
+
- **Append**: `>>` downloads the file, appends, re-uploads with rev-based conflict detection
|
|
122
139
|
- **Case sensitivity**: Dropbox paths are case-insensitive (`/README.md` and `/readme.md` are the same file)
|
|
123
140
|
- **Rate limits**: Handled automatically with retry + backoff; heavy usage may still hit Dropbox's per-user limits
|
|
124
|
-
- **No glob via `getAllPaths`**: Returns `[]` — glob matching works through `readdir` + `stat` instead
|
|
125
141
|
|
|
126
142
|
## Error handling
|
|
127
143
|
|
|
@@ -134,6 +150,9 @@ Errors are mapped to errno-style codes for natural bash output:
|
|
|
134
150
|
| `path/not_file` | `EISDIR` | Expected file, got directory |
|
|
135
151
|
| `path/not_folder` | `ENOTDIR` | Expected directory, got file |
|
|
136
152
|
| `insufficient_quota` | `ENOSPC` | Dropbox quota exceeded |
|
|
153
|
+
| HTTP 401 | `EACCES` | Token expired or invalid |
|
|
154
|
+
| HTTP 403 | `EACCES` | Insufficient app permissions |
|
|
155
|
+
| `rm /dir` (no `-r`) | `EISDIR` | Use `rm -r` to delete directories |
|
|
137
156
|
|
|
138
157
|
```ts
|
|
139
158
|
import { FsError } from "just-bash-dropbox";
|
package/dist/dropbox-fs.d.ts
CHANGED
|
@@ -1,38 +1,21 @@
|
|
|
1
|
+
import type { CpOptions, FileContent, FsStat, MkdirOptions, RmOptions } from "just-bash";
|
|
1
2
|
import type { DropboxFsOptions } from "./types.js";
|
|
2
|
-
interface FsStat {
|
|
3
|
-
isFile: boolean;
|
|
4
|
-
isDirectory: boolean;
|
|
5
|
-
isSymbolicLink: boolean;
|
|
6
|
-
mode: number;
|
|
7
|
-
size: number;
|
|
8
|
-
mtime: Date;
|
|
9
|
-
}
|
|
10
3
|
interface DirentEntry {
|
|
11
4
|
name: string;
|
|
12
5
|
isFile: boolean;
|
|
13
6
|
isDirectory: boolean;
|
|
14
7
|
isSymbolicLink: boolean;
|
|
15
8
|
}
|
|
16
|
-
interface MkdirOptions {
|
|
17
|
-
recursive?: boolean;
|
|
18
|
-
}
|
|
19
|
-
interface RmOptions {
|
|
20
|
-
recursive?: boolean;
|
|
21
|
-
force?: boolean;
|
|
22
|
-
}
|
|
23
|
-
interface CpOptions {
|
|
24
|
-
recursive?: boolean;
|
|
25
|
-
}
|
|
26
9
|
interface ReadFileOptions {
|
|
27
10
|
encoding?: string | null;
|
|
28
11
|
}
|
|
29
12
|
interface WriteFileOptions {
|
|
30
13
|
encoding?: string;
|
|
31
14
|
}
|
|
32
|
-
type FileContent = string | Uint8Array;
|
|
33
15
|
export declare class DropboxFs {
|
|
34
16
|
private readonly client;
|
|
35
17
|
private readonly rootPath;
|
|
18
|
+
private cachedPaths;
|
|
36
19
|
constructor(options: DropboxFsOptions);
|
|
37
20
|
readdir(path: string): Promise<string[]>;
|
|
38
21
|
readdirWithFileTypes(path: string): Promise<DirentEntry[]>;
|
|
@@ -48,12 +31,18 @@ export declare class DropboxFs {
|
|
|
48
31
|
mv(src: string, dest: string): Promise<void>;
|
|
49
32
|
resolvePath(base: string, target: string): string;
|
|
50
33
|
getAllPaths(): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Prefetch all file and folder paths via recursive list_folder.
|
|
36
|
+
* Call this before operations that need getAllPaths() (like glob).
|
|
37
|
+
* Results are cached until the next call to prefetchAllPaths().
|
|
38
|
+
*/
|
|
39
|
+
prefetchAllPaths(): Promise<string[]>;
|
|
51
40
|
chmod(_path: string, _mode: number): Promise<void>;
|
|
52
41
|
symlink(_target: string, _linkPath: string): Promise<void>;
|
|
53
42
|
link(_existingPath: string, _newPath: string): Promise<void>;
|
|
54
43
|
readlink(_path: string): Promise<string>;
|
|
55
|
-
lstat(
|
|
56
|
-
realpath(
|
|
44
|
+
lstat(path: string): Promise<FsStat>;
|
|
45
|
+
realpath(path: string): Promise<string>;
|
|
57
46
|
utimes(_path: string, _atime: Date, _mtime: Date): Promise<void>;
|
|
58
47
|
private toPath;
|
|
59
48
|
private listFolder;
|
package/dist/dropbox-fs.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { DropboxApiError, DropboxClient } from "./dropbox-client.js";
|
|
2
|
-
import { enosys, FsError, mapDropboxError } from "./errors.js";
|
|
2
|
+
import { eisdir, enosys, FsError, mapDropboxError } from "./errors.js";
|
|
3
3
|
import { resolvePath as resolvePathUtil, toDropboxPath } from "./paths.js";
|
|
4
4
|
export class DropboxFs {
|
|
5
5
|
client;
|
|
6
6
|
rootPath;
|
|
7
|
+
cachedPaths = [];
|
|
7
8
|
constructor(options) {
|
|
8
9
|
this.client = new DropboxClient({
|
|
9
10
|
accessToken: options.accessToken,
|
|
@@ -94,23 +95,40 @@ export class DropboxFs {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
async appendFile(path, content, _options) {
|
|
98
|
+
const dbxPath = this.toPath(path);
|
|
99
|
+
// Download existing file with metadata (to get rev for optimistic locking)
|
|
97
100
|
let existing;
|
|
101
|
+
let rev;
|
|
98
102
|
try {
|
|
99
|
-
|
|
103
|
+
const result = await this.client.contentDownload("/2/files/download", {
|
|
104
|
+
path: dbxPath,
|
|
105
|
+
});
|
|
106
|
+
existing = result.content;
|
|
107
|
+
rev = result.metadata.rev;
|
|
100
108
|
}
|
|
101
109
|
catch (err) {
|
|
102
|
-
if (err instanceof
|
|
110
|
+
if (err instanceof DropboxApiError &&
|
|
111
|
+
err.errorSummary.startsWith("path/not_found")) {
|
|
103
112
|
// File doesn't exist — create it
|
|
104
113
|
await this.writeFile(path, content);
|
|
105
114
|
return;
|
|
106
115
|
}
|
|
107
|
-
throw err;
|
|
116
|
+
throw mapDropboxError(err, path);
|
|
108
117
|
}
|
|
109
118
|
const appendBytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
110
119
|
const combined = new Uint8Array(existing.length + appendBytes.length);
|
|
111
120
|
combined.set(existing, 0);
|
|
112
121
|
combined.set(appendBytes, existing.length);
|
|
113
|
-
|
|
122
|
+
// Upload with rev-based optimistic locking to detect concurrent writes
|
|
123
|
+
const mode = rev
|
|
124
|
+
? { ".tag": "update", update: rev }
|
|
125
|
+
: "overwrite";
|
|
126
|
+
try {
|
|
127
|
+
await this.client.contentUpload("/2/files/upload", { path: dbxPath, mode, autorename: false, mute: true }, combined);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
throw mapDropboxError(err, path);
|
|
131
|
+
}
|
|
114
132
|
}
|
|
115
133
|
async mkdir(path, options) {
|
|
116
134
|
const dbxPath = this.toPath(path);
|
|
@@ -127,6 +145,20 @@ export class DropboxFs {
|
|
|
127
145
|
}
|
|
128
146
|
async rm(path, options) {
|
|
129
147
|
const dbxPath = this.toPath(path);
|
|
148
|
+
// POSIX: rm without -r on a directory should fail
|
|
149
|
+
if (!options?.recursive) {
|
|
150
|
+
try {
|
|
151
|
+
const s = await this.stat(path);
|
|
152
|
+
if (s.isDirectory) {
|
|
153
|
+
throw eisdir(path);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
if (err instanceof FsError && err.code === "EISDIR")
|
|
158
|
+
throw err;
|
|
159
|
+
// If stat fails (e.g. ENOENT), let delete_v2 handle it below
|
|
160
|
+
}
|
|
161
|
+
}
|
|
130
162
|
try {
|
|
131
163
|
await this.client.rpc("/2/files/delete_v2", { path: dbxPath });
|
|
132
164
|
}
|
|
@@ -170,7 +202,33 @@ export class DropboxFs {
|
|
|
170
202
|
return resolvePathUtil(base, target);
|
|
171
203
|
}
|
|
172
204
|
getAllPaths() {
|
|
173
|
-
return
|
|
205
|
+
return this.cachedPaths;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Prefetch all file and folder paths via recursive list_folder.
|
|
209
|
+
* Call this before operations that need getAllPaths() (like glob).
|
|
210
|
+
* Results are cached until the next call to prefetchAllPaths().
|
|
211
|
+
*/
|
|
212
|
+
async prefetchAllPaths() {
|
|
213
|
+
const dbxPath = this.toPath("/");
|
|
214
|
+
const allPaths = [];
|
|
215
|
+
try {
|
|
216
|
+
let result = await this.client.rpc("/2/files/list_folder", { path: dbxPath, recursive: true, include_deleted: false });
|
|
217
|
+
for (const entry of result.entries) {
|
|
218
|
+
allPaths.push(entry.path_display);
|
|
219
|
+
}
|
|
220
|
+
while (result.has_more) {
|
|
221
|
+
result = await this.client.rpc("/2/files/list_folder/continue", { cursor: result.cursor });
|
|
222
|
+
for (const entry of result.entries) {
|
|
223
|
+
allPaths.push(entry.path_display);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
throw mapDropboxError(err, "/");
|
|
229
|
+
}
|
|
230
|
+
this.cachedPaths = allPaths;
|
|
231
|
+
return allPaths;
|
|
174
232
|
}
|
|
175
233
|
// ─── Unsupported operations ─────────────────────────
|
|
176
234
|
async chmod(_path, _mode) {
|
|
@@ -185,11 +243,24 @@ export class DropboxFs {
|
|
|
185
243
|
async readlink(_path) {
|
|
186
244
|
throw enosys("readlink");
|
|
187
245
|
}
|
|
188
|
-
async lstat(
|
|
189
|
-
|
|
246
|
+
async lstat(path) {
|
|
247
|
+
// Dropbox has no symlinks — lstat is identical to stat
|
|
248
|
+
return this.stat(path);
|
|
190
249
|
}
|
|
191
|
-
async realpath(
|
|
192
|
-
|
|
250
|
+
async realpath(path) {
|
|
251
|
+
const dbxPath = this.toPath(path);
|
|
252
|
+
// Root has no metadata endpoint
|
|
253
|
+
if (dbxPath === "" || dbxPath === "/") {
|
|
254
|
+
return "/";
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const metadata = await this.client.rpc("/2/files/get_metadata", { path: dbxPath });
|
|
258
|
+
// Return canonical casing from Dropbox
|
|
259
|
+
return metadata.path_display;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
throw mapDropboxError(err, path);
|
|
263
|
+
}
|
|
193
264
|
}
|
|
194
265
|
async utimes(_path, _atime, _mtime) {
|
|
195
266
|
throw enosys("utimes");
|
|
@@ -251,7 +322,7 @@ function metadataToStat(metadata) {
|
|
|
251
322
|
isSymbolicLink: false,
|
|
252
323
|
mode: 0o755,
|
|
253
324
|
size: 0,
|
|
254
|
-
mtime: new Date(),
|
|
325
|
+
mtime: new Date(0),
|
|
255
326
|
};
|
|
256
327
|
case "deleted":
|
|
257
328
|
throw new FsError("ENOENT", "no such file or directory (deleted)");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { DropboxApiError, DropboxClient } from "./dropbox-client.js";
|
|
2
1
|
export { DropboxFs } from "./dropbox-fs.js";
|
|
3
2
|
export { FsError } from "./errors.js";
|
|
4
|
-
export type {
|
|
3
|
+
export type { DropboxFsOptions } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "just-bash-dropbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Dropbox filesystem adapter for just-bash",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@biomejs/biome": "^2.3.10",
|
|
50
50
|
"@types/node": "^22.0.0",
|
|
51
51
|
"typescript": "^5.9.3",
|
|
52
|
+
"just-bash": "^2.14.0",
|
|
52
53
|
"vitest": "^4.0.16"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|