just-bash-dropbox 0.1.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/AGENTS.md +84 -0
- package/README.md +188 -0
- package/dist/dropbox-client.d.ts +38 -0
- package/dist/dropbox-client.js +126 -0
- package/dist/dropbox-fs.d.ts +62 -0
- package/dist/dropbox-fs.js +259 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +73 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +73 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.js +1 -0
- package/package.json +62 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# AGENTS.md - just-bash-dropbox
|
|
2
|
+
|
|
3
|
+
Instructions for AI agents using just-bash-dropbox in projects.
|
|
4
|
+
|
|
5
|
+
## What is just-bash-dropbox?
|
|
6
|
+
|
|
7
|
+
A Dropbox filesystem adapter for [just-bash](https://github.com/vercel-labs/just-bash). It implements the `IFileSystem` interface so you can access Dropbox files through bash commands — `ls`, `cat`, `grep`, `awk`, and everything else just-bash supports.
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { Bash } from "just-bash";
|
|
13
|
+
import { DropboxFs } from "just-bash-dropbox";
|
|
14
|
+
|
|
15
|
+
const fs = new DropboxFs({ accessToken: process.env.DROPBOX_TOKEN });
|
|
16
|
+
const bash = new Bash({ fs });
|
|
17
|
+
|
|
18
|
+
const result = await bash.exec("cat /Documents/report.csv | head -5");
|
|
19
|
+
// result.stdout - file content
|
|
20
|
+
// result.stderr - error output
|
|
21
|
+
// result.exitCode - 0 = success
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Key Behaviors
|
|
25
|
+
|
|
26
|
+
1. **Every operation is an API call**: `readdir`, `stat`, `readFile` each hit the Dropbox API. There is no local cache. For repeated reads, wrap with `OverlayFs`.
|
|
27
|
+
|
|
28
|
+
2. **Paths are case-insensitive**: `/README.md` and `/readme.md` are the same file. This matches Dropbox behavior.
|
|
29
|
+
|
|
30
|
+
3. **Write operations are real**: `echo "x" > /file` uploads to Dropbox. Use `MountableFs` for safe, memory-only writes.
|
|
31
|
+
|
|
32
|
+
4. **Token expiry**: Access tokens expire after ~4 hours. Use `getAccessToken` for long sessions.
|
|
33
|
+
|
|
34
|
+
5. **Rate limits**: Automatically retried with backoff. Avoid tight loops over large directories.
|
|
35
|
+
|
|
36
|
+
## Safe Mode
|
|
37
|
+
|
|
38
|
+
**Recommended for untrusted code.** Mount Dropbox at a path, writes go to in-memory base:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { Bash, MountableFs } from "just-bash";
|
|
42
|
+
import { DropboxFs } from "just-bash-dropbox";
|
|
43
|
+
|
|
44
|
+
const dropbox = new DropboxFs({ accessToken: "..." });
|
|
45
|
+
const mfs = new MountableFs();
|
|
46
|
+
mfs.mount("/dropbox", dropbox);
|
|
47
|
+
const bash = new Bash({ fs: mfs });
|
|
48
|
+
// reads /dropbox/* from Dropbox, writes anywhere else go to memory
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Scoping
|
|
52
|
+
|
|
53
|
+
Restrict access to a subfolder:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const fs = new DropboxFs({
|
|
57
|
+
accessToken: "...",
|
|
58
|
+
rootPath: "/work/project-x",
|
|
59
|
+
});
|
|
60
|
+
// Agent sees "/" but can only access /work/project-x and below
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Unsupported Operations
|
|
64
|
+
|
|
65
|
+
These throw `ENOSYS` — Dropbox doesn't support them:
|
|
66
|
+
|
|
67
|
+
- `chmod`, `ln`, `ln -s`, `readlink`, `realpath`
|
|
68
|
+
|
|
69
|
+
## Error Codes
|
|
70
|
+
|
|
71
|
+
| Code | Meaning |
|
|
72
|
+
|------|---------|
|
|
73
|
+
| `ENOENT` | File/directory not found |
|
|
74
|
+
| `EEXIST` | Already exists |
|
|
75
|
+
| `EISDIR` | Is a directory (expected file) |
|
|
76
|
+
| `ENOTDIR` | Not a directory (expected directory) |
|
|
77
|
+
| `ENOSPC` | Dropbox quota exceeded |
|
|
78
|
+
| `ENOSYS` | Operation not supported |
|
|
79
|
+
|
|
80
|
+
## Limitations
|
|
81
|
+
|
|
82
|
+
- Single uploads: 150 MB max
|
|
83
|
+
- `>>` (append): downloads, appends, re-uploads — slow for large files
|
|
84
|
+
- `getAllPaths()` returns `[]` — glob works via readdir + stat
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# just-bash-dropbox
|
|
2
|
+
|
|
3
|
+
> **Beta** — API may change. Use at your own risk.
|
|
4
|
+
|
|
5
|
+
A Dropbox filesystem adapter for [just-bash](https://github.com/vercel-labs/just-bash). Lets AI agents access Dropbox files through bash commands.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Bash } from "just-bash";
|
|
9
|
+
import { DropboxFs } from "just-bash-dropbox";
|
|
10
|
+
|
|
11
|
+
const fs = new DropboxFs({ accessToken: process.env.DROPBOX_TOKEN });
|
|
12
|
+
const bash = new Bash({ fs });
|
|
13
|
+
|
|
14
|
+
const { stdout } = await bash.exec("ls /Documents");
|
|
15
|
+
// "report.csv\nphotos\n"
|
|
16
|
+
|
|
17
|
+
await bash.exec("cat /reports/q4-summary.md");
|
|
18
|
+
await bash.exec("grep 'revenue' /reports/*.csv | sort -t, -k2 -rn");
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install just-bash-dropbox just-bash
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No other dependencies. Uses native `fetch` — Node 18+.
|
|
28
|
+
|
|
29
|
+
## Authentication
|
|
30
|
+
|
|
31
|
+
`DropboxFs` needs a Dropbox access token. Provide it directly or via a function that returns a fresh token.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// Static token — scripts, testing, short-lived sessions
|
|
35
|
+
const fs = new DropboxFs({ accessToken: "sl...." });
|
|
36
|
+
|
|
37
|
+
// Token provider — production, long-running agents
|
|
38
|
+
// Called before each API request. You handle refresh and persistence.
|
|
39
|
+
const fs = new DropboxFs({
|
|
40
|
+
getAccessToken: async () => {
|
|
41
|
+
return await myTokenStore.getFreshToken(userId);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Get a token: create a Dropbox app at [dropbox.com/developers](https://www.dropbox.com/developers) and generate an access token, or implement the [OAuth2 flow](https://www.dropbox.com/developers/documentation/http/documentation#authorization).
|
|
47
|
+
|
|
48
|
+
Access tokens expire after ~4 hours. For agents that run longer, use `getAccessToken` with a refresh token flow (see `examples/token-provider.ts`).
|
|
49
|
+
|
|
50
|
+
## Scoping to a folder
|
|
51
|
+
|
|
52
|
+
Restrict the filesystem to a Dropbox subfolder. The agent sees `/` but operations are scoped underneath. Use this to limit what an agent can access.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const fs = new DropboxFs({
|
|
56
|
+
accessToken: "...",
|
|
57
|
+
rootPath: "/work/project-x",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const bash = new Bash({ fs });
|
|
61
|
+
await bash.exec("ls /"); // lists /work/project-x
|
|
62
|
+
await bash.exec("cat /readme.md"); // reads /work/project-x/readme.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Safe mode (read-only mount)
|
|
66
|
+
|
|
67
|
+
Use just-bash's `MountableFs` to mount Dropbox at a path while keeping the rest in memory. Writes go to the in-memory base filesystem, not to Dropbox. **Recommended for untrusted agents.**
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { Bash, MountableFs } from "just-bash";
|
|
71
|
+
import { DropboxFs } from "just-bash-dropbox";
|
|
72
|
+
|
|
73
|
+
const dropbox = new DropboxFs({ accessToken: "..." });
|
|
74
|
+
const mfs = new MountableFs();
|
|
75
|
+
mfs.mount("/dropbox", dropbox);
|
|
76
|
+
const bash = new Bash({ fs: mfs });
|
|
77
|
+
|
|
78
|
+
await bash.exec("cat /dropbox/data.csv"); // reads from Dropbox
|
|
79
|
+
await bash.exec("echo 'hello' > /notes.txt"); // writes to in-memory fs
|
|
80
|
+
await bash.exec("cp /dropbox/data.csv /local-copy.csv"); // copies to memory
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## API
|
|
84
|
+
|
|
85
|
+
### `new DropboxFs(options)`
|
|
86
|
+
|
|
87
|
+
| Option | Type | Default | Description |
|
|
88
|
+
|--------|------|---------|-------------|
|
|
89
|
+
| `accessToken` | `string` | — | Static Dropbox access token |
|
|
90
|
+
| `getAccessToken` | `() => string \| Promise<string>` | — | Returns a fresh token per request |
|
|
91
|
+
| `rootPath` | `string` | `"/"` (root) | Scope all operations to this folder |
|
|
92
|
+
|
|
93
|
+
Provide either `accessToken` or `getAccessToken`, not both.
|
|
94
|
+
|
|
95
|
+
### Supported operations
|
|
96
|
+
|
|
97
|
+
All standard bash file operations work:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ls /path # list directory
|
|
101
|
+
cat /path/file.txt # read file
|
|
102
|
+
echo "content" > /path/file.txt # write file
|
|
103
|
+
cp /src /dest # copy
|
|
104
|
+
mv /src /dest # move
|
|
105
|
+
rm /path/file.txt # delete
|
|
106
|
+
rm -r /path/dir # delete directory
|
|
107
|
+
mkdir -p /path/deep/nested # create nested directories
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Not supported
|
|
111
|
+
|
|
112
|
+
Dropbox doesn't have these concepts. These operations throw `ENOSYS`:
|
|
113
|
+
|
|
114
|
+
- `chmod` — no file permissions
|
|
115
|
+
- `ln` / `ln -s` — no hard/symbolic links
|
|
116
|
+
- `readlink`, `realpath` — no symlinks to resolve
|
|
117
|
+
|
|
118
|
+
### Limitations
|
|
119
|
+
|
|
120
|
+
- **File size**: Single uploads are limited to 150 MB
|
|
121
|
+
- **Append**: `>>` works but downloads the entire file, appends, and re-uploads
|
|
122
|
+
- **Case sensitivity**: Dropbox paths are case-insensitive (`/README.md` and `/readme.md` are the same file)
|
|
123
|
+
- **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
|
+
|
|
126
|
+
## Error handling
|
|
127
|
+
|
|
128
|
+
Errors are mapped to errno-style codes for natural bash output:
|
|
129
|
+
|
|
130
|
+
| Dropbox error | Mapped code | Meaning |
|
|
131
|
+
|---------------|-------------|---------|
|
|
132
|
+
| `path/not_found` | `ENOENT` | File or directory doesn't exist |
|
|
133
|
+
| `path/conflict` | `EEXIST` | Already exists |
|
|
134
|
+
| `path/not_file` | `EISDIR` | Expected file, got directory |
|
|
135
|
+
| `path/not_folder` | `ENOTDIR` | Expected directory, got file |
|
|
136
|
+
| `insufficient_quota` | `ENOSPC` | Dropbox quota exceeded |
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { FsError } from "just-bash-dropbox";
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await fs.readFile("/missing.txt");
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (err instanceof FsError && err.code === "ENOENT") {
|
|
145
|
+
// file not found
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Interactive CLI
|
|
151
|
+
|
|
152
|
+
Chat with an AI that has full access to your Dropbox:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
DROPBOX_TOKEN=sl.xxx AI_GATEWAY_API_KEY=xxx npx tsx examples/chat-cli.ts
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
you: what files do I have?
|
|
160
|
+
$ ls -la /
|
|
161
|
+
welcome.md
|
|
162
|
+
|
|
163
|
+
ai: You have one file — `welcome.md`. Want me to read it?
|
|
164
|
+
|
|
165
|
+
you: create a project plan in /plan.md
|
|
166
|
+
$ echo "# Project Plan" > /plan.md
|
|
167
|
+
$ echo "## Phase 1: Research" >> /plan.md
|
|
168
|
+
|
|
169
|
+
ai: Created /plan.md with a project plan outline.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
See [`examples/chat-cli.ts`](./examples/chat-cli.ts) for the implementation — it's ~100 lines using the Vercel AI SDK.
|
|
173
|
+
|
|
174
|
+
## Examples
|
|
175
|
+
|
|
176
|
+
See the [`examples/`](./examples) directory:
|
|
177
|
+
|
|
178
|
+
- **`chat-cli.ts`** — Interactive CLI chat agent (recommended starting point)
|
|
179
|
+
- **`ai-agent-sdk.ts`** — Single-prompt AI agent with Vercel AI SDK
|
|
180
|
+
- **`safe-mode.ts`** — Read-only mount with `MountableFs`
|
|
181
|
+
- **`token-provider.ts`** — Production auth with auto-refresh
|
|
182
|
+
- **`basic-browsing.ts`** — List and read files
|
|
183
|
+
- **`file-search.ts`** — `grep` and `awk` on Dropbox files
|
|
184
|
+
- **`ai-agent.ts`** — Scripted agent workflow (no LLM)
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
Apache-2.0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DropboxFsOptions } from "./types.js";
|
|
2
|
+
export declare class DropboxApiError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly errorSummary: string;
|
|
5
|
+
readonly errorBody: unknown;
|
|
6
|
+
constructor(status: number, errorSummary: string, errorBody: unknown);
|
|
7
|
+
}
|
|
8
|
+
export declare class DropboxClient {
|
|
9
|
+
private readonly getToken;
|
|
10
|
+
constructor(options: Pick<DropboxFsOptions, "accessToken" | "getAccessToken">);
|
|
11
|
+
/**
|
|
12
|
+
* RPC-style request: JSON in body, JSON response.
|
|
13
|
+
* Used for list_folder, get_metadata, create_folder, delete, copy, move.
|
|
14
|
+
*/
|
|
15
|
+
rpc<T>(endpoint: string, args: Record<string, unknown>): Promise<T>;
|
|
16
|
+
/**
|
|
17
|
+
* Content-upload: args in Dropbox-API-Arg header, file bytes in body.
|
|
18
|
+
* Used for upload.
|
|
19
|
+
*/
|
|
20
|
+
contentUpload<T>(endpoint: string, args: Record<string, unknown>, content: Uint8Array): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* Content-download: args in Dropbox-API-Arg header, file bytes in response.
|
|
23
|
+
* Used for download.
|
|
24
|
+
*/
|
|
25
|
+
contentDownload(endpoint: string, args: Record<string, unknown>): Promise<{
|
|
26
|
+
metadata: Record<string, unknown>;
|
|
27
|
+
content: Uint8Array;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Low-level request with retry on 429.
|
|
31
|
+
* Parses JSON response and throws DropboxApiError on error status.
|
|
32
|
+
*/
|
|
33
|
+
private request;
|
|
34
|
+
/**
|
|
35
|
+
* Raw request with retry logic. Returns the Response object.
|
|
36
|
+
*/
|
|
37
|
+
private requestRaw;
|
|
38
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const RPC_HOST = "https://api.dropboxapi.com";
|
|
2
|
+
const CONTENT_HOST = "https://content.dropboxapi.com";
|
|
3
|
+
const MAX_RETRIES = 3;
|
|
4
|
+
export class DropboxApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
errorSummary;
|
|
7
|
+
errorBody;
|
|
8
|
+
constructor(status, errorSummary, errorBody) {
|
|
9
|
+
super(errorSummary);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.errorSummary = errorSummary;
|
|
12
|
+
this.errorBody = errorBody;
|
|
13
|
+
this.name = "DropboxApiError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class DropboxClient {
|
|
17
|
+
getToken;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
if (options.accessToken && options.getAccessToken) {
|
|
20
|
+
throw new Error("Provide either accessToken or getAccessToken, not both");
|
|
21
|
+
}
|
|
22
|
+
if (!options.accessToken && !options.getAccessToken) {
|
|
23
|
+
throw new Error("Provide either accessToken or getAccessToken");
|
|
24
|
+
}
|
|
25
|
+
this.getToken =
|
|
26
|
+
options.getAccessToken ?? (() => options.accessToken);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* RPC-style request: JSON in body, JSON response.
|
|
30
|
+
* Used for list_folder, get_metadata, create_folder, delete, copy, move.
|
|
31
|
+
*/
|
|
32
|
+
async rpc(endpoint, args) {
|
|
33
|
+
return this.request(async (token) => {
|
|
34
|
+
const response = await fetch(`${RPC_HOST}${endpoint}`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${token}`,
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(args),
|
|
41
|
+
});
|
|
42
|
+
return response;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Content-upload: args in Dropbox-API-Arg header, file bytes in body.
|
|
47
|
+
* Used for upload.
|
|
48
|
+
*/
|
|
49
|
+
async contentUpload(endpoint, args, content) {
|
|
50
|
+
return this.request(async (token) => {
|
|
51
|
+
const response = await fetch(`${CONTENT_HOST}${endpoint}`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${token}`,
|
|
55
|
+
"Content-Type": "application/octet-stream",
|
|
56
|
+
"Dropbox-API-Arg": JSON.stringify(args),
|
|
57
|
+
},
|
|
58
|
+
body: content,
|
|
59
|
+
});
|
|
60
|
+
return response;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Content-download: args in Dropbox-API-Arg header, file bytes in response.
|
|
65
|
+
* Used for download.
|
|
66
|
+
*/
|
|
67
|
+
async contentDownload(endpoint, args) {
|
|
68
|
+
const response = await this.requestRaw(async (token) => {
|
|
69
|
+
const resp = await fetch(`${CONTENT_HOST}${endpoint}`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: `Bearer ${token}`,
|
|
73
|
+
"Dropbox-API-Arg": JSON.stringify(args),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return resp;
|
|
77
|
+
});
|
|
78
|
+
const resultHeader = response.headers.get("Dropbox-API-Result");
|
|
79
|
+
const metadata = resultHeader ? JSON.parse(resultHeader) : {};
|
|
80
|
+
const content = new Uint8Array(await response.arrayBuffer());
|
|
81
|
+
return { metadata, content };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Low-level request with retry on 429.
|
|
85
|
+
* Parses JSON response and throws DropboxApiError on error status.
|
|
86
|
+
*/
|
|
87
|
+
async request(doFetch) {
|
|
88
|
+
const response = await this.requestRaw(doFetch);
|
|
89
|
+
return response.json();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Raw request with retry logic. Returns the Response object.
|
|
93
|
+
*/
|
|
94
|
+
async requestRaw(doFetch) {
|
|
95
|
+
let lastError;
|
|
96
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
97
|
+
const token = await this.getToken();
|
|
98
|
+
const response = await doFetch(token);
|
|
99
|
+
if (response.ok) {
|
|
100
|
+
return response;
|
|
101
|
+
}
|
|
102
|
+
if (response.status === 429) {
|
|
103
|
+
const parsed = Number(response.headers.get("Retry-After"));
|
|
104
|
+
const retryAfter = Number.isFinite(parsed) ? parsed : 1;
|
|
105
|
+
// Consume body to avoid leak
|
|
106
|
+
await response.text();
|
|
107
|
+
await sleep(retryAfter * 1000);
|
|
108
|
+
lastError = new Error("Rate limited");
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Parse error body for 409 and other errors
|
|
112
|
+
const body = await response.json().catch(() => ({}));
|
|
113
|
+
const summary = typeof body === "object" &&
|
|
114
|
+
body !== null &&
|
|
115
|
+
"error_summary" in body &&
|
|
116
|
+
typeof body.error_summary === "string"
|
|
117
|
+
? body.error_summary
|
|
118
|
+
: `HTTP ${response.status}`;
|
|
119
|
+
throw new DropboxApiError(response.status, summary, body);
|
|
120
|
+
}
|
|
121
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function sleep(ms) {
|
|
125
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
126
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
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
|
+
interface DirentEntry {
|
|
11
|
+
name: string;
|
|
12
|
+
isFile: boolean;
|
|
13
|
+
isDirectory: boolean;
|
|
14
|
+
isSymbolicLink: boolean;
|
|
15
|
+
}
|
|
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
|
+
interface ReadFileOptions {
|
|
27
|
+
encoding?: string | null;
|
|
28
|
+
}
|
|
29
|
+
interface WriteFileOptions {
|
|
30
|
+
encoding?: string;
|
|
31
|
+
}
|
|
32
|
+
type FileContent = string | Uint8Array;
|
|
33
|
+
export declare class DropboxFs {
|
|
34
|
+
private readonly client;
|
|
35
|
+
private readonly rootPath;
|
|
36
|
+
constructor(options: DropboxFsOptions);
|
|
37
|
+
readdir(path: string): Promise<string[]>;
|
|
38
|
+
readdirWithFileTypes(path: string): Promise<DirentEntry[]>;
|
|
39
|
+
readFile(path: string, _options?: ReadFileOptions | string): Promise<string>;
|
|
40
|
+
readFileBuffer(path: string): Promise<Uint8Array>;
|
|
41
|
+
stat(path: string): Promise<FsStat>;
|
|
42
|
+
exists(path: string): Promise<boolean>;
|
|
43
|
+
writeFile(path: string, content: FileContent, _options?: WriteFileOptions | string): Promise<void>;
|
|
44
|
+
appendFile(path: string, content: FileContent, _options?: WriteFileOptions | string): Promise<void>;
|
|
45
|
+
mkdir(path: string, options?: MkdirOptions): Promise<void>;
|
|
46
|
+
rm(path: string, options?: RmOptions): Promise<void>;
|
|
47
|
+
cp(src: string, dest: string, _options?: CpOptions): Promise<void>;
|
|
48
|
+
mv(src: string, dest: string): Promise<void>;
|
|
49
|
+
resolvePath(base: string, target: string): string;
|
|
50
|
+
getAllPaths(): string[];
|
|
51
|
+
chmod(_path: string, _mode: number): Promise<void>;
|
|
52
|
+
symlink(_target: string, _linkPath: string): Promise<void>;
|
|
53
|
+
link(_existingPath: string, _newPath: string): Promise<void>;
|
|
54
|
+
readlink(_path: string): Promise<string>;
|
|
55
|
+
lstat(_path: string): Promise<FsStat>;
|
|
56
|
+
realpath(_path: string): Promise<string>;
|
|
57
|
+
utimes(_path: string, _atime: Date, _mtime: Date): Promise<void>;
|
|
58
|
+
private toPath;
|
|
59
|
+
private listFolder;
|
|
60
|
+
private mkdirRecursive;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { DropboxApiError, DropboxClient } from "./dropbox-client.js";
|
|
2
|
+
import { enosys, FsError, mapDropboxError } from "./errors.js";
|
|
3
|
+
import { resolvePath as resolvePathUtil, toDropboxPath } from "./paths.js";
|
|
4
|
+
export class DropboxFs {
|
|
5
|
+
client;
|
|
6
|
+
rootPath;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.client = new DropboxClient({
|
|
9
|
+
accessToken: options.accessToken,
|
|
10
|
+
getAccessToken: options.getAccessToken,
|
|
11
|
+
});
|
|
12
|
+
this.rootPath = options.rootPath;
|
|
13
|
+
}
|
|
14
|
+
// ─── Read operations ────────────────────────────────
|
|
15
|
+
async readdir(path) {
|
|
16
|
+
const entries = await this.listFolder(path);
|
|
17
|
+
return entries.map((e) => e.name);
|
|
18
|
+
}
|
|
19
|
+
async readdirWithFileTypes(path) {
|
|
20
|
+
const entries = await this.listFolder(path);
|
|
21
|
+
return entries.map((e) => ({
|
|
22
|
+
name: e.name,
|
|
23
|
+
isFile: e[".tag"] === "file",
|
|
24
|
+
isDirectory: e[".tag"] === "folder",
|
|
25
|
+
isSymbolicLink: false,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
async readFile(path, _options) {
|
|
29
|
+
try {
|
|
30
|
+
const dbxPath = this.toPath(path);
|
|
31
|
+
const result = await this.client.contentDownload("/2/files/download", {
|
|
32
|
+
path: dbxPath,
|
|
33
|
+
});
|
|
34
|
+
return new TextDecoder().decode(result.content);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
throw mapDropboxError(err, path);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async readFileBuffer(path) {
|
|
41
|
+
try {
|
|
42
|
+
const dbxPath = this.toPath(path);
|
|
43
|
+
const result = await this.client.contentDownload("/2/files/download", {
|
|
44
|
+
path: dbxPath,
|
|
45
|
+
});
|
|
46
|
+
return result.content;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw mapDropboxError(err, path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async stat(path) {
|
|
53
|
+
const dbxPath = this.toPath(path);
|
|
54
|
+
// Dropbox API does not support get_metadata on root folder
|
|
55
|
+
if (dbxPath === "" || dbxPath === "/") {
|
|
56
|
+
return {
|
|
57
|
+
isFile: false,
|
|
58
|
+
isDirectory: true,
|
|
59
|
+
isSymbolicLink: false,
|
|
60
|
+
mode: 0o755,
|
|
61
|
+
size: 0,
|
|
62
|
+
mtime: new Date(0),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const metadata = await this.client.rpc("/2/files/get_metadata", { path: dbxPath });
|
|
67
|
+
return metadataToStat(metadata);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw mapDropboxError(err, path);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async exists(path) {
|
|
74
|
+
try {
|
|
75
|
+
await this.stat(path);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ─── Write operations ───────────────────────────────
|
|
86
|
+
async writeFile(path, content, _options) {
|
|
87
|
+
const dbxPath = this.toPath(path);
|
|
88
|
+
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
89
|
+
try {
|
|
90
|
+
await this.client.contentUpload("/2/files/upload", { path: dbxPath, mode: "overwrite", autorename: false, mute: true }, bytes);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
throw mapDropboxError(err, path);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async appendFile(path, content, _options) {
|
|
97
|
+
let existing;
|
|
98
|
+
try {
|
|
99
|
+
existing = await this.readFileBuffer(path);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
103
|
+
// File doesn't exist — create it
|
|
104
|
+
await this.writeFile(path, content);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
const appendBytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
110
|
+
const combined = new Uint8Array(existing.length + appendBytes.length);
|
|
111
|
+
combined.set(existing, 0);
|
|
112
|
+
combined.set(appendBytes, existing.length);
|
|
113
|
+
await this.writeFile(path, combined);
|
|
114
|
+
}
|
|
115
|
+
async mkdir(path, options) {
|
|
116
|
+
const dbxPath = this.toPath(path);
|
|
117
|
+
if (options?.recursive) {
|
|
118
|
+
await this.mkdirRecursive(dbxPath, path);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await this.client.rpc("/2/files/create_folder_v2", { path: dbxPath });
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
throw mapDropboxError(err, path);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async rm(path, options) {
|
|
129
|
+
const dbxPath = this.toPath(path);
|
|
130
|
+
try {
|
|
131
|
+
await this.client.rpc("/2/files/delete_v2", { path: dbxPath });
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (options?.force &&
|
|
135
|
+
err instanceof DropboxApiError &&
|
|
136
|
+
err.errorSummary.startsWith("path_lookup/not_found")) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
throw mapDropboxError(err, path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async cp(src, dest, _options) {
|
|
143
|
+
const fromPath = this.toPath(src);
|
|
144
|
+
const toPath = this.toPath(dest);
|
|
145
|
+
try {
|
|
146
|
+
await this.client.rpc("/2/files/copy_v2", {
|
|
147
|
+
from_path: fromPath,
|
|
148
|
+
to_path: toPath,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
throw mapDropboxError(err, src);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async mv(src, dest) {
|
|
156
|
+
const fromPath = this.toPath(src);
|
|
157
|
+
const toPath = this.toPath(dest);
|
|
158
|
+
try {
|
|
159
|
+
await this.client.rpc("/2/files/move_v2", {
|
|
160
|
+
from_path: fromPath,
|
|
161
|
+
to_path: toPath,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
throw mapDropboxError(err, src);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ─── Path operations ───────────────────────────────
|
|
169
|
+
resolvePath(base, target) {
|
|
170
|
+
return resolvePathUtil(base, target);
|
|
171
|
+
}
|
|
172
|
+
getAllPaths() {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
// ─── Unsupported operations ─────────────────────────
|
|
176
|
+
async chmod(_path, _mode) {
|
|
177
|
+
throw enosys("chmod");
|
|
178
|
+
}
|
|
179
|
+
async symlink(_target, _linkPath) {
|
|
180
|
+
throw enosys("symlink");
|
|
181
|
+
}
|
|
182
|
+
async link(_existingPath, _newPath) {
|
|
183
|
+
throw enosys("link");
|
|
184
|
+
}
|
|
185
|
+
async readlink(_path) {
|
|
186
|
+
throw enosys("readlink");
|
|
187
|
+
}
|
|
188
|
+
async lstat(_path) {
|
|
189
|
+
throw enosys("lstat");
|
|
190
|
+
}
|
|
191
|
+
async realpath(_path) {
|
|
192
|
+
throw enosys("realpath");
|
|
193
|
+
}
|
|
194
|
+
async utimes(_path, _atime, _mtime) {
|
|
195
|
+
throw enosys("utimes");
|
|
196
|
+
}
|
|
197
|
+
// ─── Private helpers ───────────────────────────────
|
|
198
|
+
toPath(path) {
|
|
199
|
+
return toDropboxPath(path, this.rootPath);
|
|
200
|
+
}
|
|
201
|
+
async listFolder(path) {
|
|
202
|
+
const dbxPath = this.toPath(path);
|
|
203
|
+
const allEntries = [];
|
|
204
|
+
try {
|
|
205
|
+
let result = await this.client.rpc("/2/files/list_folder", { path: dbxPath, include_deleted: false });
|
|
206
|
+
allEntries.push(...result.entries);
|
|
207
|
+
while (result.has_more) {
|
|
208
|
+
result = await this.client.rpc("/2/files/list_folder/continue", { cursor: result.cursor });
|
|
209
|
+
allEntries.push(...result.entries);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
throw mapDropboxError(err, path);
|
|
214
|
+
}
|
|
215
|
+
return allEntries;
|
|
216
|
+
}
|
|
217
|
+
async mkdirRecursive(dbxPath, originalPath) {
|
|
218
|
+
// Build list of paths to create from root to leaf
|
|
219
|
+
const parts = dbxPath.split("/").filter(Boolean);
|
|
220
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
221
|
+
const segment = `/${parts.slice(0, i).join("/")}`;
|
|
222
|
+
try {
|
|
223
|
+
await this.client.rpc("/2/files/create_folder_v2", { path: segment });
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
// Ignore "already exists" errors during recursive creation
|
|
227
|
+
if (err instanceof DropboxApiError &&
|
|
228
|
+
err.errorSummary.startsWith("path/conflict")) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
throw mapDropboxError(err, originalPath);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function metadataToStat(metadata) {
|
|
237
|
+
switch (metadata[".tag"]) {
|
|
238
|
+
case "file":
|
|
239
|
+
return {
|
|
240
|
+
isFile: true,
|
|
241
|
+
isDirectory: false,
|
|
242
|
+
isSymbolicLink: false,
|
|
243
|
+
mode: 0o644,
|
|
244
|
+
size: metadata.size,
|
|
245
|
+
mtime: new Date(metadata.server_modified),
|
|
246
|
+
};
|
|
247
|
+
case "folder":
|
|
248
|
+
return {
|
|
249
|
+
isFile: false,
|
|
250
|
+
isDirectory: true,
|
|
251
|
+
isSymbolicLink: false,
|
|
252
|
+
mode: 0o755,
|
|
253
|
+
size: 0,
|
|
254
|
+
mtime: new Date(),
|
|
255
|
+
};
|
|
256
|
+
case "deleted":
|
|
257
|
+
throw new FsError("ENOENT", "no such file or directory (deleted)");
|
|
258
|
+
}
|
|
259
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps Dropbox API errors to filesystem-style errors.
|
|
3
|
+
* Uses errno-like codes so bash commands produce natural error output.
|
|
4
|
+
*/
|
|
5
|
+
export declare class FsError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly path?: string | undefined;
|
|
8
|
+
constructor(code: string, message: string, path?: string | undefined);
|
|
9
|
+
}
|
|
10
|
+
export declare function enoent(path: string): FsError;
|
|
11
|
+
export declare function enotdir(path: string): FsError;
|
|
12
|
+
export declare function eisdir(path: string): FsError;
|
|
13
|
+
export declare function eexist(path: string): FsError;
|
|
14
|
+
export declare function enosys(operation: string): FsError;
|
|
15
|
+
export declare function enospc(path: string): FsError;
|
|
16
|
+
/**
|
|
17
|
+
* Maps a DropboxApiError to a filesystem error.
|
|
18
|
+
* Uses error_summary prefix matching as recommended by Dropbox docs.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mapDropboxError(err: unknown, path: string): FsError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { DropboxApiError } from "./dropbox-client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Maps Dropbox API errors to filesystem-style errors.
|
|
4
|
+
* Uses errno-like codes so bash commands produce natural error output.
|
|
5
|
+
*/
|
|
6
|
+
export class FsError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
path;
|
|
9
|
+
constructor(code, message, path) {
|
|
10
|
+
super(path ? `${code}: ${message}, '${path}'` : `${code}: ${message}`);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.path = path;
|
|
13
|
+
this.name = "FsError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function enoent(path) {
|
|
17
|
+
return new FsError("ENOENT", "no such file or directory", path);
|
|
18
|
+
}
|
|
19
|
+
export function enotdir(path) {
|
|
20
|
+
return new FsError("ENOTDIR", "not a directory", path);
|
|
21
|
+
}
|
|
22
|
+
export function eisdir(path) {
|
|
23
|
+
return new FsError("EISDIR", "is a directory", path);
|
|
24
|
+
}
|
|
25
|
+
export function eexist(path) {
|
|
26
|
+
return new FsError("EEXIST", "file already exists", path);
|
|
27
|
+
}
|
|
28
|
+
export function enosys(operation) {
|
|
29
|
+
return new FsError("ENOSYS", `${operation} is not supported on Dropbox`);
|
|
30
|
+
}
|
|
31
|
+
export function enospc(path) {
|
|
32
|
+
return new FsError("ENOSPC", "no space left (Dropbox quota exceeded)", path);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Maps a DropboxApiError to a filesystem error.
|
|
36
|
+
* Uses error_summary prefix matching as recommended by Dropbox docs.
|
|
37
|
+
*/
|
|
38
|
+
export function mapDropboxError(err, path) {
|
|
39
|
+
if (!(err instanceof DropboxApiError)) {
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
const summary = err.errorSummary;
|
|
43
|
+
if (summary.startsWith("path/not_found") ||
|
|
44
|
+
summary.startsWith("path_lookup/not_found")) {
|
|
45
|
+
return enoent(path);
|
|
46
|
+
}
|
|
47
|
+
if (summary.startsWith("path/conflict") ||
|
|
48
|
+
summary.startsWith("to/conflict")) {
|
|
49
|
+
return eexist(path);
|
|
50
|
+
}
|
|
51
|
+
if (summary.startsWith("path/not_file")) {
|
|
52
|
+
return eisdir(path);
|
|
53
|
+
}
|
|
54
|
+
if (summary.startsWith("path/not_folder")) {
|
|
55
|
+
return enotdir(path);
|
|
56
|
+
}
|
|
57
|
+
if (summary.startsWith("path/insufficient_space") ||
|
|
58
|
+
summary.startsWith("insufficient_quota")) {
|
|
59
|
+
return enospc(path);
|
|
60
|
+
}
|
|
61
|
+
if (summary.startsWith("from_lookup/not_found")) {
|
|
62
|
+
return enoent(path);
|
|
63
|
+
}
|
|
64
|
+
// HTTP status-based mapping
|
|
65
|
+
if (err.status === 401) {
|
|
66
|
+
return new FsError("EACCES", "access token expired or invalid — refresh your token", path);
|
|
67
|
+
}
|
|
68
|
+
if (err.status === 403) {
|
|
69
|
+
return new FsError("EACCES", "access denied — check app permissions", path);
|
|
70
|
+
}
|
|
71
|
+
// For unmapped errors, wrap as-is with the path context
|
|
72
|
+
return new FsError("EIO", summary, path);
|
|
73
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path normalization for Dropbox API.
|
|
3
|
+
*
|
|
4
|
+
* Dropbox quirks:
|
|
5
|
+
* - Root is "" (empty string), not "/"
|
|
6
|
+
* - All other paths start with "/"
|
|
7
|
+
* - Paths are case-insensitive but case-preserving
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a path for Dropbox API consumption.
|
|
11
|
+
* Removes trailing slashes, collapses doubles, handles root.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toDropboxPath(path: string, rootPath?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a relative path against a base path.
|
|
16
|
+
* Pure function — no Dropbox API calls.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolvePath(base: string, target: string): string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path normalization for Dropbox API.
|
|
3
|
+
*
|
|
4
|
+
* Dropbox quirks:
|
|
5
|
+
* - Root is "" (empty string), not "/"
|
|
6
|
+
* - All other paths start with "/"
|
|
7
|
+
* - Paths are case-insensitive but case-preserving
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a path for Dropbox API consumption.
|
|
11
|
+
* Removes trailing slashes, collapses doubles, handles root.
|
|
12
|
+
*/
|
|
13
|
+
export function toDropboxPath(path, rootPath) {
|
|
14
|
+
// Normalize separators and collapse doubles
|
|
15
|
+
let normalized = path.replace(/\/+/g, "/");
|
|
16
|
+
// Remove trailing slash (unless root)
|
|
17
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
18
|
+
normalized = normalized.slice(0, -1);
|
|
19
|
+
}
|
|
20
|
+
// Apply rootPath prefix
|
|
21
|
+
if (rootPath) {
|
|
22
|
+
const cleanRoot = rootPath.replace(/\/+$/, "");
|
|
23
|
+
if (normalized === "/") {
|
|
24
|
+
normalized = cleanRoot;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
normalized = `${cleanRoot}${normalized}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Dropbox root is "" not "/"
|
|
31
|
+
if (normalized === "/") {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
// Ensure path starts with /
|
|
35
|
+
if (!normalized.startsWith("/")) {
|
|
36
|
+
normalized = `/${normalized}`;
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a relative path against a base path.
|
|
42
|
+
* Pure function — no Dropbox API calls.
|
|
43
|
+
*/
|
|
44
|
+
export function resolvePath(base, target) {
|
|
45
|
+
// Absolute path — ignore base
|
|
46
|
+
if (target.startsWith("/")) {
|
|
47
|
+
return normalizePath(target);
|
|
48
|
+
}
|
|
49
|
+
// Relative path — join with base
|
|
50
|
+
const combined = base.endsWith("/")
|
|
51
|
+
? `${base}${target}`
|
|
52
|
+
: `${base}/${target}`;
|
|
53
|
+
return normalizePath(combined);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Normalize a path: resolve . and .., collapse separators.
|
|
57
|
+
*/
|
|
58
|
+
function normalizePath(path) {
|
|
59
|
+
const parts = path.split("/");
|
|
60
|
+
const resolved = [];
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
if (part === "" || part === ".") {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (part === "..") {
|
|
66
|
+
resolved.pop();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
resolved.push(part);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return `/${resolved.join("/")}`;
|
|
73
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for creating a DropboxFs instance.
|
|
3
|
+
*/
|
|
4
|
+
export interface DropboxFsOptions {
|
|
5
|
+
/** Static Dropbox access token. */
|
|
6
|
+
accessToken?: string;
|
|
7
|
+
/** Function that returns a fresh access token. Called before each API request. */
|
|
8
|
+
getAccessToken?: () => string | Promise<string>;
|
|
9
|
+
/** Scope all operations to this Dropbox folder (default: root). */
|
|
10
|
+
rootPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DropboxFileMetadata {
|
|
13
|
+
".tag": "file";
|
|
14
|
+
name: string;
|
|
15
|
+
id: string;
|
|
16
|
+
path_lower: string;
|
|
17
|
+
path_display: string;
|
|
18
|
+
rev: string;
|
|
19
|
+
size: number;
|
|
20
|
+
client_modified: string;
|
|
21
|
+
server_modified: string;
|
|
22
|
+
content_hash?: string;
|
|
23
|
+
is_downloadable?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface DropboxFolderMetadata {
|
|
26
|
+
".tag": "folder";
|
|
27
|
+
name: string;
|
|
28
|
+
id: string;
|
|
29
|
+
path_lower: string;
|
|
30
|
+
path_display: string;
|
|
31
|
+
}
|
|
32
|
+
export interface DropboxDeletedMetadata {
|
|
33
|
+
".tag": "deleted";
|
|
34
|
+
name: string;
|
|
35
|
+
path_lower: string;
|
|
36
|
+
path_display: string;
|
|
37
|
+
}
|
|
38
|
+
export type DropboxMetadata = DropboxFileMetadata | DropboxFolderMetadata | DropboxDeletedMetadata;
|
|
39
|
+
export interface DropboxListFolderResult {
|
|
40
|
+
entries: DropboxMetadata[];
|
|
41
|
+
cursor: string;
|
|
42
|
+
has_more: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface DropboxErrorResponse {
|
|
45
|
+
error_summary: string;
|
|
46
|
+
error: {
|
|
47
|
+
".tag": string;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
};
|
|
50
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "just-bash-dropbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dropbox filesystem adapter for just-bash",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/manishrc/just-bash-dropbox.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"README.md",
|
|
21
|
+
"AGENTS.md"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "rm -rf dist && tsc",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"test": "vitest",
|
|
33
|
+
"test:run": "vitest run",
|
|
34
|
+
"lint": "biome check .",
|
|
35
|
+
"lint:fix": "biome check --write .",
|
|
36
|
+
"validate": "biome check . && tsc --noEmit && vitest run && rm -rf dist && tsc",
|
|
37
|
+
"prepublishOnly": "npm run validate"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"dropbox",
|
|
41
|
+
"bash",
|
|
42
|
+
"filesystem",
|
|
43
|
+
"ai-agent",
|
|
44
|
+
"just-bash"
|
|
45
|
+
],
|
|
46
|
+
"author": "",
|
|
47
|
+
"license": "Apache-2.0",
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@biomejs/biome": "^2.3.10",
|
|
50
|
+
"@types/node": "^22.0.0",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vitest": "^4.0.16"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"just-bash": ">=2.0.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"just-bash": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|