@homecloud-platform/sdk 0.4.9 → 0.5.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/src/so.js ADDED
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const { soObjectPaths } = require("./signing");
7
+ const { HomeCloudError } = require("./errors");
8
+
9
+ class SoAPI {
10
+ constructor(client) {
11
+ this._c = client;
12
+ }
13
+
14
+ async listBuckets() {
15
+ this._c.requireConsole();
16
+ const data = await this._c.consoleRequest(
17
+ "GET",
18
+ `accounts/${this._c.accountId}/storage/buckets`
19
+ );
20
+ return data.items || [];
21
+ }
22
+
23
+ async createBucket(name) {
24
+ this._c.requireConsole();
25
+ return this._c.consoleRequest("POST", `accounts/${this._c.accountId}/storage/buckets`, {
26
+ json: { name: String(name).trim().toLowerCase() },
27
+ });
28
+ }
29
+
30
+ async deleteBucket(name) {
31
+ this._c.requireConsole();
32
+ await this._c.consoleRequest(
33
+ "DELETE",
34
+ `accounts/${this._c.accountId}/storage/buckets/${String(name).trim().toLowerCase()}`
35
+ );
36
+ }
37
+
38
+ async listObjects(bucketName, { prefix = "", recursive = false, page = 1, pageSize = 100 } = {}) {
39
+ this._c.requireAccessKey();
40
+ const accountId = this._c.accountId;
41
+ const reqPath = `/${accountId}/${bucketName}/objects`;
42
+ return this._c.dataPlaneRequest("so", "GET", reqPath, {
43
+ params: { prefix, recursive, page, page_size: pageSize },
44
+ });
45
+ }
46
+
47
+ async listAllObjects(bucketName, { prefix = "", recursive = true } = {}) {
48
+ const items = [];
49
+ let page = 1;
50
+ for (;;) {
51
+ const data = await this.listObjects(bucketName, { prefix, recursive, page, pageSize: 100 });
52
+ for (const item of data.items || []) {
53
+ if (!item.is_dir) items.push(item);
54
+ }
55
+ if (page >= Number(data.pages || 1)) break;
56
+ page += 1;
57
+ }
58
+ return items;
59
+ }
60
+
61
+ async upload(bucketName, filePath, { key } = {}) {
62
+ this._c.requireAccessKey();
63
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
64
+ throw new HomeCloudError(`File not found: ${filePath}`);
65
+ }
66
+ const objectKey = key || path.basename(filePath);
67
+ const accountId = this._c.accountId;
68
+ const uploadPath = `/${accountId}/${bucketName}/objects`;
69
+ const blob = fs.readFileSync(filePath);
70
+ const form = new FormData();
71
+ form.append("key", objectKey);
72
+ form.append("file", new Blob([blob]), path.basename(filePath));
73
+ return this._c.dataPlaneRequest("so", "POST", uploadPath, { formData: form });
74
+ }
75
+
76
+ async putJson(bucketName, objectKey, value) {
77
+ const tmp = path.join(
78
+ os.tmpdir(),
79
+ `hc-sdk-${Date.now()}-${Math.random().toString(16).slice(2)}.json`
80
+ );
81
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2), "utf8");
82
+ try {
83
+ return await this.upload(bucketName, tmp, { key: objectKey });
84
+ } finally {
85
+ try {
86
+ fs.unlinkSync(tmp);
87
+ } catch (_) {}
88
+ }
89
+ }
90
+
91
+ async delete(bucketName, objectKey) {
92
+ this._c.requireAccessKey();
93
+ const { signPath, urlPath } = soObjectPaths(this._c.accountId, bucketName, objectKey);
94
+ await this._c.dataPlaneRequest("so", "DELETE", signPath, { urlPath, signPath });
95
+ }
96
+
97
+ async download(bucketName, objectKey, { destPath } = {}) {
98
+ this._c.requireAccessKey();
99
+ if (!destPath) throw new HomeCloudError("destPath is required");
100
+ const key = String(objectKey).replace(/^\/+/, "");
101
+ const { signPath, urlPath } = soObjectPaths(this._c.accountId, bucketName, key);
102
+ const buf = await this._c.dataPlaneRequestBytes("so", "GET", signPath, { urlPath, signPath });
103
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
104
+ fs.writeFileSync(destPath, buf);
105
+ return { key, size: buf.length, path: destPath };
106
+ }
107
+
108
+ async headObject(bucketName, objectKey) {
109
+ this._c.requireAccessKey();
110
+ const key = String(objectKey).replace(/^\/+/, "");
111
+ const { signPath, urlPath } = soObjectPaths(this._c.accountId, bucketName, key);
112
+ const raw = await this._c.dataPlaneRequest("so", "GET", `${signPath}/metadata`, {
113
+ urlPath: `${urlPath}/metadata`,
114
+ signPath: `${signPath}/metadata`,
115
+ });
116
+ if (!raw || typeof raw !== "object") throw new HomeCloudError("Invalid metadata response");
117
+ const userMeta = raw.metadata && typeof raw.metadata === "object" ? raw.metadata : {};
118
+ const tags = raw.tags && typeof raw.tags === "object" ? raw.tags : {};
119
+ return {
120
+ key: String(raw.key || key),
121
+ size: Number(raw.size || 0),
122
+ etag: raw.etag,
123
+ content_type: raw.content_type,
124
+ last_modified: raw.last_modified,
125
+ metadata: Object.fromEntries(Object.entries(userMeta).map(([k, v]) => [String(k), String(v)])),
126
+ tags: Object.fromEntries(Object.entries(tags).map(([k, v]) => [String(k), String(v)])),
127
+ };
128
+ }
129
+
130
+ async objectMetadata(bucketName, objectKey) {
131
+ return this.headObject(bucketName, objectKey);
132
+ }
133
+
134
+ async getObjectUri(bucketName, objectKey) {
135
+ this._c.requireAccessKey();
136
+ const key = String(objectKey).replace(/^\/+/, "");
137
+ const { signPath, urlPath } = soObjectPaths(this._c.accountId, bucketName, key);
138
+ const raw = await this._c.dataPlaneRequest("so", "GET", `${signPath}/uri`, {
139
+ urlPath: `${urlPath}/uri`,
140
+ signPath: `${signPath}/uri`,
141
+ });
142
+ if (!raw || typeof raw !== "object") throw new HomeCloudError("Invalid URI response");
143
+ return {
144
+ so_uri: String(raw.so_uri || `so://${bucketName}/${key}`),
145
+ https_url: String(raw.https_url || ""),
146
+ https_requires_public: Boolean(raw.https_requires_public ?? true),
147
+ };
148
+ }
149
+
150
+ async generatePresignedUrl(bucketName, objectKey, { expires = 3600 } = {}) {
151
+ this._c.requireAccessKey();
152
+ const key = String(objectKey).replace(/^\/+/, "");
153
+ const { signPath, urlPath } = soObjectPaths(this._c.accountId, bucketName, key);
154
+ const raw = await this._c.dataPlaneRequest("so", "GET", `${signPath}/presigned`, {
155
+ urlPath: `${urlPath}/presigned`,
156
+ signPath: `${signPath}/presigned`,
157
+ params: { expires },
158
+ });
159
+ if (!raw || !raw.url) throw new HomeCloudError("Invalid presigned URL response");
160
+ return {
161
+ url: String(raw.url),
162
+ expires_in_seconds: Number(raw.expires_in_seconds || expires),
163
+ };
164
+ }
165
+
166
+ async deleteRecursive(bucketName, prefix = "") {
167
+ const items = await this.listAllObjects(bucketName, { prefix, recursive: true });
168
+ for (const item of items) {
169
+ await this.delete(bucketName, item.key);
170
+ }
171
+ return items.length;
172
+ }
173
+
174
+ async syncLocalToBucket(localDir, bucketName, { prefix = "", deleteExtra = false } = {}) {
175
+ this._c.requireAccessKey();
176
+ const root = path.resolve(localDir);
177
+ const prefixClean = String(prefix || "").replace(/^\/+|\/+$/g, "");
178
+ const walk = (dir, base) => {
179
+ const out = [];
180
+ for (const name of fs.readdirSync(dir)) {
181
+ const full = path.join(dir, name);
182
+ const st = fs.statSync(full);
183
+ const rel = path.relative(base, full).split(path.sep).join("/");
184
+ if (st.isDirectory()) out.push(...walk(full, base));
185
+ else out.push(rel);
186
+ }
187
+ return out;
188
+ };
189
+ const locals = walk(root, root);
190
+ for (const rel of locals) {
191
+ const key = prefixClean ? `${prefixClean}/${rel}` : rel;
192
+ await this.upload(bucketName, path.join(root, rel), { key });
193
+ }
194
+ if (deleteExtra) {
195
+ const remote = await this.listAllObjects(bucketName, { prefix: prefixClean, recursive: true });
196
+ const localKeys = new Set(
197
+ locals.map((rel) => (prefixClean ? `${prefixClean}/${rel}` : rel))
198
+ );
199
+ for (const item of remote) {
200
+ if (!localKeys.has(item.key)) await this.delete(bucketName, item.key);
201
+ }
202
+ }
203
+ return { uploaded: locals.length };
204
+ }
205
+
206
+ async syncBucketToLocal(bucketName, localDir, { prefix = "" } = {}) {
207
+ this._c.requireAccessKey();
208
+ const root = path.resolve(localDir);
209
+ const prefixClean = String(prefix || "").replace(/^\/+|\/+$/g, "");
210
+ const items = await this.listAllObjects(bucketName, { prefix: prefixClean, recursive: true });
211
+ for (const item of items) {
212
+ let rel = item.key;
213
+ if (prefixClean && item.key.startsWith(`${prefixClean}/`)) {
214
+ rel = item.key.slice(prefixClean.length + 1);
215
+ } else if (prefixClean && item.key === prefixClean) {
216
+ rel = path.basename(item.key);
217
+ }
218
+ const dest = path.join(root, rel);
219
+ await this.download(bucketName, item.key, { destPath: dest });
220
+ }
221
+ return { downloaded: items.length };
222
+ }
223
+ }
224
+
225
+ module.exports = { SoAPI };