@putkoff/abstract-securefiles 0.0.53 → 0.0.54
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/dist/People.d.ts +9 -0
- package/dist/api.d.ts +26 -1
- package/dist/index.cjs +163 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +163 -14
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +26 -0
- package/package.json +1 -1
- package/src/People.tsx +327 -0
- package/src/SecureFiles.tsx +5 -0
- package/src/api.ts +32 -1
- package/src/index.ts +1 -1
- package/src/styles.css +4 -0
- package/src/types.ts +26 -0
package/dist/types.d.ts
CHANGED
|
@@ -72,3 +72,29 @@ export interface SecureFilesProps {
|
|
|
72
72
|
/** API origin; defaults to https://api.abstractendeavors.com */
|
|
73
73
|
apiBase?: string;
|
|
74
74
|
}
|
|
75
|
+
export interface Person {
|
|
76
|
+
username: string;
|
|
77
|
+
connected_at: string | null;
|
|
78
|
+
/** what I share with this person: everything, or specific item ids */
|
|
79
|
+
my_grant: {
|
|
80
|
+
all: boolean;
|
|
81
|
+
ids: number[];
|
|
82
|
+
};
|
|
83
|
+
/** how many items they share with me */
|
|
84
|
+
shared_with_me: number;
|
|
85
|
+
}
|
|
86
|
+
export interface SharedItem {
|
|
87
|
+
id: number;
|
|
88
|
+
filename: string;
|
|
89
|
+
filepath: string;
|
|
90
|
+
kind: "file" | "folder";
|
|
91
|
+
size: number | null;
|
|
92
|
+
available: boolean;
|
|
93
|
+
device: Device | null;
|
|
94
|
+
created_at: string | null;
|
|
95
|
+
}
|
|
96
|
+
export interface SharedOwner {
|
|
97
|
+
owner: string;
|
|
98
|
+
all: boolean;
|
|
99
|
+
files: SharedItem[];
|
|
100
|
+
}
|
package/package.json
CHANGED
package/src/People.tsx
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import type { SecureFilesApi } from "./api";
|
|
3
|
+
import type { Person, SecureFile, SharedOwner } from "./types";
|
|
4
|
+
import { ConfirmButton } from "./ConfirmButton";
|
|
5
|
+
import { CopyButton } from "./CopyButton";
|
|
6
|
+
|
|
7
|
+
const errText = (err: unknown) => (err instanceof Error ? err.message : String(err));
|
|
8
|
+
|
|
9
|
+
function formatSize(bytes: number | null) {
|
|
10
|
+
if (bytes == null) return "";
|
|
11
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
12
|
+
let n = bytes;
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
15
|
+
n /= 1024;
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
return `${n.toFixed(i ? 1 : 0)} ${units[i]}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function GrantEditor({
|
|
22
|
+
person,
|
|
23
|
+
files,
|
|
24
|
+
onSave,
|
|
25
|
+
onClose,
|
|
26
|
+
}: {
|
|
27
|
+
person: Person;
|
|
28
|
+
files: SecureFile[];
|
|
29
|
+
onSave: (grant: { all: boolean; ids: number[] }) => Promise<void>;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
}) {
|
|
32
|
+
const [all, setAll] = useState(person.my_grant.all);
|
|
33
|
+
const [ids, setIds] = useState<Set<number>>(new Set(person.my_grant.ids));
|
|
34
|
+
const [search, setSearch] = useState("");
|
|
35
|
+
const [saving, setSaving] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
const visible = useMemo(() => {
|
|
38
|
+
const q = search.trim().toLowerCase();
|
|
39
|
+
return [...files]
|
|
40
|
+
.sort((a, b) => a.filepath.localeCompare(b.filepath))
|
|
41
|
+
.filter((f) => !q || f.filepath.toLowerCase().includes(q));
|
|
42
|
+
}, [files, search]);
|
|
43
|
+
|
|
44
|
+
const save = async () => {
|
|
45
|
+
setSaving(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
try {
|
|
48
|
+
await onSave({ all, ids: all ? [] : [...ids] });
|
|
49
|
+
onClose();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
setError(errText(err));
|
|
52
|
+
} finally {
|
|
53
|
+
setSaving(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="sf-card">
|
|
59
|
+
<h4>What {person.username} can access</h4>
|
|
60
|
+
<label className="sf-check">
|
|
61
|
+
<input type="radio" checked={all} onChange={() => setAll(true)} />
|
|
62
|
+
<span>
|
|
63
|
+
<b>All my files</b> <span className="sf-muted">— including anything you add later</span>
|
|
64
|
+
</span>
|
|
65
|
+
</label>
|
|
66
|
+
<label className="sf-check">
|
|
67
|
+
<input type="radio" checked={!all} onChange={() => setAll(false)} />
|
|
68
|
+
<b>Only the items I choose</b>
|
|
69
|
+
</label>
|
|
70
|
+
{!all && (
|
|
71
|
+
<>
|
|
72
|
+
<input
|
|
73
|
+
className="sf-input"
|
|
74
|
+
type="search"
|
|
75
|
+
placeholder="Filter your files…"
|
|
76
|
+
value={search}
|
|
77
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
78
|
+
/>
|
|
79
|
+
<div className="sf-browser">
|
|
80
|
+
{visible.length === 0 && <p className="sf-muted">No files.</p>}
|
|
81
|
+
{visible.map((f) => (
|
|
82
|
+
<label key={f.id} className="sf-check sf-browser__file">
|
|
83
|
+
<input
|
|
84
|
+
type="checkbox"
|
|
85
|
+
checked={ids.has(f.id)}
|
|
86
|
+
onChange={(e) =>
|
|
87
|
+
setIds((s) => {
|
|
88
|
+
const next = new Set(s);
|
|
89
|
+
e.target.checked ? next.add(f.id) : next.delete(f.id);
|
|
90
|
+
return next;
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
/>
|
|
94
|
+
<span className="sf-browser__name">{f.filepath}</span>
|
|
95
|
+
<span className="sf-muted">{f.kind === "folder" ? "folder · .tar" : formatSize(f.size)}</span>
|
|
96
|
+
</label>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
<span className="sf-muted">{ids.size} selected</span>
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
{error && <p className="sf-error">{error}</p>}
|
|
103
|
+
<div className="sf-row sf-row--end">
|
|
104
|
+
<button type="button" className="sf-btn" onClick={onClose}>
|
|
105
|
+
Cancel
|
|
106
|
+
</button>
|
|
107
|
+
<button type="button" className="sf-btn sf-btn--primary" disabled={saving} onClick={save}>
|
|
108
|
+
{saving ? "Saving…" : "Save"}
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function People({ api, files }: { api: SecureFilesApi; files: SecureFile[] }) {
|
|
116
|
+
const [people, setPeople] = useState<Person[]>([]);
|
|
117
|
+
const [code, setCode] = useState<string | null>(null);
|
|
118
|
+
const [entered, setEntered] = useState("");
|
|
119
|
+
const [editing, setEditing] = useState<string | null>(null);
|
|
120
|
+
const [error, setError] = useState<string | null>(null);
|
|
121
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
122
|
+
|
|
123
|
+
const load = useCallback(async () => {
|
|
124
|
+
try {
|
|
125
|
+
setPeople((await api.people()).people);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
setError(errText(err));
|
|
128
|
+
}
|
|
129
|
+
}, [api]);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
load();
|
|
133
|
+
}, [load]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!code) return;
|
|
137
|
+
const timer = setInterval(load, 5000);
|
|
138
|
+
const expire = setTimeout(() => setCode(null), 15 * 60 * 1000);
|
|
139
|
+
return () => {
|
|
140
|
+
clearInterval(timer);
|
|
141
|
+
clearTimeout(expire);
|
|
142
|
+
};
|
|
143
|
+
}, [code, load]);
|
|
144
|
+
|
|
145
|
+
const invite = async () => {
|
|
146
|
+
setError(null);
|
|
147
|
+
try {
|
|
148
|
+
setCode((await api.invitePerson()).code);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
setError(errText(err));
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const accept = async () => {
|
|
155
|
+
setError(null);
|
|
156
|
+
setNotice(null);
|
|
157
|
+
try {
|
|
158
|
+
const res = await api.acceptPerson(entered);
|
|
159
|
+
setPeople(res.people);
|
|
160
|
+
setEntered("");
|
|
161
|
+
setNotice(`Connected with ${res.connected_with}.`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
setError(errText(err));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const describe = (p: Person) =>
|
|
168
|
+
p.my_grant.all ? "can access all your files" : `can access ${p.my_grant.ids.length} of your items`;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<details className="sf-admin">
|
|
172
|
+
<summary>
|
|
173
|
+
People <span className="sf-muted">({people.length})</span>
|
|
174
|
+
</summary>
|
|
175
|
+
<div className="sf-stack">
|
|
176
|
+
<p className="sf-muted">
|
|
177
|
+
Connect with other abstractendeavors.com accounts, then choose what each person can access: all your files
|
|
178
|
+
or specific ones. They download while signed in; nothing is public.
|
|
179
|
+
</p>
|
|
180
|
+
{people.length > 0 && (
|
|
181
|
+
<ul className="sf-roots">
|
|
182
|
+
{people.map((p) => (
|
|
183
|
+
<li key={p.username}>
|
|
184
|
+
<span>
|
|
185
|
+
<b>{p.username}</b>{" "}
|
|
186
|
+
<span className={p.my_grant.all ? "sf-tag sf-tag--warn" : "sf-muted"}>{describe(p)}</span>
|
|
187
|
+
{p.shared_with_me > 0 && (
|
|
188
|
+
<span className="sf-muted"> · shares {p.shared_with_me} with you</span>
|
|
189
|
+
)}
|
|
190
|
+
</span>
|
|
191
|
+
<span className="sf-row">
|
|
192
|
+
<button type="button" className="sf-btn" onClick={() => setEditing(p.username)}>
|
|
193
|
+
Choose access
|
|
194
|
+
</button>
|
|
195
|
+
<ConfirmButton
|
|
196
|
+
label="Disconnect"
|
|
197
|
+
onConfirm={async () => {
|
|
198
|
+
try {
|
|
199
|
+
setPeople((await api.removePerson(p.username)).people);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
setError(errText(err));
|
|
202
|
+
}
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
</span>
|
|
206
|
+
</li>
|
|
207
|
+
))}
|
|
208
|
+
</ul>
|
|
209
|
+
)}
|
|
210
|
+
{editing && (
|
|
211
|
+
<GrantEditor
|
|
212
|
+
key={editing}
|
|
213
|
+
person={people.find((p) => p.username === editing)!}
|
|
214
|
+
files={files}
|
|
215
|
+
onClose={() => setEditing(null)}
|
|
216
|
+
onSave={async (grant) => setPeople((await api.setGrants(editing, grant)).people)}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
<div className="sf-row sf-row--bottom">
|
|
220
|
+
{code ? (
|
|
221
|
+
<div className="sf-field">
|
|
222
|
+
<span>Send this code to the person (valid 15 minutes, once):</span>
|
|
223
|
+
<div className="sf-row">
|
|
224
|
+
<span className="sf-paircode">{code}</span>
|
|
225
|
+
<CopyButton text={code} label="Copy" />
|
|
226
|
+
<button type="button" className="sf-btn" onClick={() => setCode(null)}>
|
|
227
|
+
Done
|
|
228
|
+
</button>
|
|
229
|
+
</div>
|
|
230
|
+
<span className="sf-muted">They enter it under People → "Have a code?" on their Secure Files page.</span>
|
|
231
|
+
</div>
|
|
232
|
+
) : (
|
|
233
|
+
<button type="button" className="sf-btn sf-btn--primary" onClick={invite}>
|
|
234
|
+
Invite someone
|
|
235
|
+
</button>
|
|
236
|
+
)}
|
|
237
|
+
<label className="sf-field sf-field--narrow">
|
|
238
|
+
<span>Have a code?</span>
|
|
239
|
+
<div className="sf-row">
|
|
240
|
+
<input
|
|
241
|
+
className="sf-input"
|
|
242
|
+
value={entered}
|
|
243
|
+
placeholder="ABCD-1234"
|
|
244
|
+
onChange={(e) => setEntered(e.target.value)}
|
|
245
|
+
onKeyDown={(e) => e.key === "Enter" && entered.trim() && accept()}
|
|
246
|
+
/>
|
|
247
|
+
<button type="button" className="sf-btn" disabled={!entered.trim()} onClick={accept}>
|
|
248
|
+
Connect
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
</label>
|
|
252
|
+
</div>
|
|
253
|
+
{notice && <p className="sf-muted">{notice}</p>}
|
|
254
|
+
{error && <p className="sf-error">{error}</p>}
|
|
255
|
+
</div>
|
|
256
|
+
</details>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function SharedWithMe({ api }: { api: SecureFilesApi }) {
|
|
261
|
+
const [owners, setOwners] = useState<SharedOwner[] | null>(null);
|
|
262
|
+
const [error, setError] = useState<string | null>(null);
|
|
263
|
+
|
|
264
|
+
const load = useCallback(async () => {
|
|
265
|
+
try {
|
|
266
|
+
setOwners((await api.sharedWithMe()).owners);
|
|
267
|
+
setError(null);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
setError(errText(err));
|
|
270
|
+
}
|
|
271
|
+
}, [api]);
|
|
272
|
+
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
load();
|
|
275
|
+
const onVisible = () => document.visibilityState === "visible" && load();
|
|
276
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
277
|
+
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
278
|
+
}, [load]);
|
|
279
|
+
|
|
280
|
+
if (!owners || owners.length === 0) return error ? <p className="sf-error">{error}</p> : null;
|
|
281
|
+
return (
|
|
282
|
+
<section className="sf-stack">
|
|
283
|
+
<h3 className="sf-subhead">Shared with me</h3>
|
|
284
|
+
{owners.map((o) => (
|
|
285
|
+
<div key={o.owner} className="sf-table-wrap">
|
|
286
|
+
<table className="sf-table">
|
|
287
|
+
<thead>
|
|
288
|
+
<tr>
|
|
289
|
+
<th>
|
|
290
|
+
From <b>{o.owner}</b> {o.all && <span className="sf-muted">(all their files)</span>}
|
|
291
|
+
</th>
|
|
292
|
+
<th>Size</th>
|
|
293
|
+
<th className="sf-col-actions" />
|
|
294
|
+
</tr>
|
|
295
|
+
</thead>
|
|
296
|
+
<tbody>
|
|
297
|
+
{o.files.map((f) => (
|
|
298
|
+
<tr key={f.id}>
|
|
299
|
+
<td>
|
|
300
|
+
{f.filepath}
|
|
301
|
+
{f.kind === "folder" && <span className="sf-tag">folder · .tar</span>}
|
|
302
|
+
{f.device && (
|
|
303
|
+
<span className={`sf-tag${f.device.online ? "" : " sf-tag--warn"}`}>
|
|
304
|
+
on {f.device.name} · {f.device.online ? "online" : "offline"}
|
|
305
|
+
</span>
|
|
306
|
+
)}
|
|
307
|
+
</td>
|
|
308
|
+
<td className="sf-muted">{formatSize(f.size)}</td>
|
|
309
|
+
<td className="sf-col-actions">
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
className="sf-btn"
|
|
313
|
+
disabled={!f.available}
|
|
314
|
+
onClick={() => api.downloadShared(f.id)}
|
|
315
|
+
>
|
|
316
|
+
Download
|
|
317
|
+
</button>
|
|
318
|
+
</td>
|
|
319
|
+
</tr>
|
|
320
|
+
))}
|
|
321
|
+
</tbody>
|
|
322
|
+
</table>
|
|
323
|
+
</div>
|
|
324
|
+
))}
|
|
325
|
+
</section>
|
|
326
|
+
);
|
|
327
|
+
}
|
package/src/SecureFiles.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { CopyButton } from "./CopyButton";
|
|
|
7
7
|
import { ConfirmButton } from "./ConfirmButton";
|
|
8
8
|
import { ServerFiles } from "./ServerFiles";
|
|
9
9
|
import { MyComputers } from "./MyComputers";
|
|
10
|
+
import { People, SharedWithMe } from "./People";
|
|
10
11
|
import "./styles.css";
|
|
11
12
|
|
|
12
13
|
interface FolderNode {
|
|
@@ -279,6 +280,8 @@ export default function SecureFiles({ apiBase }: SecureFilesProps) {
|
|
|
279
280
|
|
|
280
281
|
<MyComputers api={api} devices={devices} onChange={load} />
|
|
281
282
|
|
|
283
|
+
<People api={api} files={files} />
|
|
284
|
+
|
|
282
285
|
{isAdmin && <ServerFiles api={api} folders={folders} onLinked={load} />}
|
|
283
286
|
|
|
284
287
|
<section className="sf-toolbar">
|
|
@@ -352,6 +355,8 @@ export default function SecureFiles({ apiBase }: SecureFilesProps) {
|
|
|
352
355
|
</div>
|
|
353
356
|
)}
|
|
354
357
|
|
|
358
|
+
<SharedWithMe api={api} />
|
|
359
|
+
|
|
355
360
|
{editing && (
|
|
356
361
|
<ShareDialog
|
|
357
362
|
key={editing.id}
|
package/src/api.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowseResult, Device, LinkRoot, RootChallenge, SecureFile, ShareUpdate } from "./types";
|
|
1
|
+
import type { BrowseResult, Device, LinkRoot, Person, RootChallenge, SecureFile, SharedOwner, ShareUpdate } from "./types";
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_API_BASE = "https://api.abstractendeavors.com";
|
|
4
4
|
|
|
@@ -42,6 +42,37 @@ export function createApi(apiBase: string = DEFAULT_API_BASE) {
|
|
|
42
42
|
|
|
43
43
|
removeDevice: (id: number) => json<{ devices: Device[] }>(`/devices/${id}`, { method: "DELETE" }),
|
|
44
44
|
|
|
45
|
+
people: () => json<{ people: Person[] }>("/people"),
|
|
46
|
+
|
|
47
|
+
invitePerson: () => json<{ code: string; expires_in: number }>("/people/invite", { method: "POST", body: "{}" }),
|
|
48
|
+
|
|
49
|
+
acceptPerson: (code: string) =>
|
|
50
|
+
json<{ connected_with: string; people: Person[] }>("/people/accept", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
body: JSON.stringify({ code }),
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
setGrants: (username: string, grant: { all: boolean; ids: number[] }) =>
|
|
56
|
+
json<{ people: Person[] }>(`/people/${encodeURIComponent(username)}/grants`, {
|
|
57
|
+
method: "PUT",
|
|
58
|
+
body: JSON.stringify(grant),
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
removePerson: (username: string) =>
|
|
62
|
+
json<{ people: Person[] }>(`/people/${encodeURIComponent(username)}`, { method: "DELETE" }),
|
|
63
|
+
|
|
64
|
+
sharedWithMe: () => json<{ owners: SharedOwner[] }>("/shared-with-me"),
|
|
65
|
+
|
|
66
|
+
/** Browser-native download of an item someone shared with me (streams to disk, resumable). */
|
|
67
|
+
downloadShared(id: number): void {
|
|
68
|
+
const a = document.createElement("a");
|
|
69
|
+
a.href = `${root}/shared-with-me/${id}`;
|
|
70
|
+
a.rel = "noopener";
|
|
71
|
+
document.body.appendChild(a);
|
|
72
|
+
a.click();
|
|
73
|
+
a.remove();
|
|
74
|
+
},
|
|
75
|
+
|
|
45
76
|
roots: () => json<{ roots: LinkRoot[] }>("/admin/roots"),
|
|
46
77
|
|
|
47
78
|
requestRoot: (path: string) =>
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { default as SecureFiles } from "./SecureFiles";
|
|
2
2
|
export { createApi, DEFAULT_API_BASE, ApiError } from "./api";
|
|
3
3
|
export type { SecureFilesApi } from "./api";
|
|
4
|
-
export type { SecureFile, ShareUpdate, SecureFilesProps, LinkRoot, RootChallenge, BrowseResult, Device } from "./types";
|
|
4
|
+
export type { SecureFile, ShareUpdate, SecureFilesProps, LinkRoot, RootChallenge, BrowseResult, Device, Person, SharedItem, SharedOwner } from "./types";
|
package/src/styles.css
CHANGED
package/src/types.ts
CHANGED
|
@@ -69,3 +69,29 @@ export interface SecureFilesProps {
|
|
|
69
69
|
/** API origin; defaults to https://api.abstractendeavors.com */
|
|
70
70
|
apiBase?: string;
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
export interface Person {
|
|
74
|
+
username: string;
|
|
75
|
+
connected_at: string | null;
|
|
76
|
+
/** what I share with this person: everything, or specific item ids */
|
|
77
|
+
my_grant: { all: boolean; ids: number[] };
|
|
78
|
+
/** how many items they share with me */
|
|
79
|
+
shared_with_me: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SharedItem {
|
|
83
|
+
id: number;
|
|
84
|
+
filename: string;
|
|
85
|
+
filepath: string;
|
|
86
|
+
kind: "file" | "folder";
|
|
87
|
+
size: number | null;
|
|
88
|
+
available: boolean;
|
|
89
|
+
device: Device | null;
|
|
90
|
+
created_at: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SharedOwner {
|
|
94
|
+
owner: string;
|
|
95
|
+
all: boolean;
|
|
96
|
+
files: SharedItem[];
|
|
97
|
+
}
|