Skip to content

Commit efb70c7

Browse files
committed
docs(changeset): Update Storage services + enable steaming
1 parent 50f460f commit efb70c7

File tree

26 files changed

+606
-258
lines changed

26 files changed

+606
-258
lines changed

.changeset/angry-papayas-jog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@storybooker/adapter-azure": minor
3+
"@storybooker/adapter-fs": minor
4+
"@storybooker/core": minor
5+
---
6+
7+
Update Storage services + enable steaming

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"build": "turbo build",
1616
"change": "changeset",
1717
"check": "turbo check",
18+
"core:build": "yarn workspace @storybooker/core build",
19+
"core:dev": "yarn workspace @storybooker/core dev",
1820
"cli": "yarn workspace storybooker start",
1921
"cli:dev": "yarn workspace storybooker dev",
2022
"dev": "turbo dev",
Lines changed: 109 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import fs from "node:fs";
2-
import path from "node:path";
31
import { Readable } from "node:stream";
42
import type streamWeb from "node:stream/web";
5-
import { BlobServiceClient, type BlobClient } from "@azure/storage-blob";
3+
import {
4+
BlobServiceClient,
5+
type BlobClient,
6+
type BlockBlobClient,
7+
} from "@azure/storage-blob";
68
import type { StorageService } from "@storybooker/core/types";
79

810
export class AzureBlobStorageService implements StorageService {
@@ -12,32 +14,63 @@ export class AzureBlobStorageService implements StorageService {
1214
this.#client = BlobServiceClient.fromConnectionString(connectionString);
1315
}
1416

15-
createContainer: StorageService["createContainer"] = async (name) => {
16-
await this.#client.createContainer(name);
17+
createContainer: StorageService["createContainer"] = async (
18+
containerId,
19+
options,
20+
) => {
21+
await this.#client.createContainer(containerId, {
22+
abortSignal: options.abortSignal,
23+
});
24+
};
25+
26+
deleteContainer: StorageService["deleteContainer"] = async (
27+
containerId,
28+
options,
29+
) => {
30+
await this.#client.getContainerClient(containerId).deleteIfExists({
31+
abortSignal: options.abortSignal,
32+
});
1733
};
1834

19-
deleteContainer: StorageService["deleteContainer"] = async (name) => {
20-
await this.#client.getContainerClient(name).deleteIfExists();
35+
hasContainer: StorageService["hasContainer"] = async (
36+
containerId,
37+
options,
38+
) => {
39+
return await this.#client.getContainerClient(containerId).exists({
40+
abortSignal: options.abortSignal,
41+
});
2142
};
2243

23-
listContainers: StorageService["listContainers"] = async () => {
44+
listContainers: StorageService["listContainers"] = async (options) => {
2445
const containers: string[] = [];
25-
for await (const item of this.#client.listContainers()) {
46+
for await (const item of this.#client.listContainers({
47+
abortSignal: options.abortSignal,
48+
})) {
2649
containers.push(item.name);
2750
}
2851

2952
return containers;
3053
};
3154

32-
deleteFile: StorageService["deleteFile"] = async (name, path) => {
33-
await this.#client.getContainerClient(name).deleteBlob(path);
34-
};
35-
36-
deleteFiles: StorageService["deleteFiles"] = async (name, prefix) => {
37-
const containerClient = this.#client.getContainerClient(name);
55+
deleteFiles: StorageService["deleteFiles"] = async (
56+
containerId,
57+
filePathsOrPrefix,
58+
options,
59+
) => {
60+
const containerClient = this.#client.getContainerClient(containerId);
3861
const blobClientsToDelete: BlobClient[] = [];
39-
for await (const blob of containerClient.listBlobsFlat({ prefix })) {
40-
blobClientsToDelete.push(containerClient.getBlobClient(blob.name));
62+
63+
if (typeof filePathsOrPrefix === "string") {
64+
for await (const blob of containerClient.listBlobsFlat({
65+
abortSignal: options.abortSignal,
66+
prefix: filePathsOrPrefix,
67+
})) {
68+
blobClientsToDelete.push(containerClient.getBlobClient(blob.name));
69+
}
70+
} else {
71+
for (const filepath of filePathsOrPrefix) {
72+
blobClientsToDelete.push(containerClient.getBlobClient(filepath));
73+
}
4174
}
4275

4376
if (blobClientsToDelete.length === 0) {
@@ -46,138 +79,109 @@ export class AzureBlobStorageService implements StorageService {
4679

4780
const response = await containerClient
4881
.getBlobBatchClient()
49-
.deleteBlobs(blobClientsToDelete);
82+
.deleteBlobs(blobClientsToDelete, {
83+
abortSignal: options.abortSignal,
84+
});
5085

5186
if (response.errorCode) {
5287
throw new Error(`Failed to delete blobs: ${response.errorCode}`);
5388
}
5489
return;
5590
};
5691

57-
uploadFile: StorageService["uploadFile"] = async (
58-
containerName,
59-
file,
92+
uploadFiles: StorageService["uploadFiles"] = async (
93+
containerId,
94+
files,
6095
options,
6196
) => {
62-
const { destinationPath, mimeType = "application/octet-stream" } = options;
63-
const client = this.#client
64-
.getContainerClient(containerName)
65-
.getBlockBlobClient(destinationPath);
97+
const containerClient = this.#client.getContainerClient(containerId);
98+
// oxlint-disable-next-line require-await
99+
const promises = files.map(async ({ content, path, mimeType }) =>
100+
this.#uploadFile(
101+
containerClient.getBlockBlobClient(path),
102+
content,
103+
mimeType,
104+
options.abortSignal,
105+
),
106+
);
107+
108+
await Promise.allSettled(promises);
109+
};
66110

67-
if (typeof file === "string") {
68-
await client.uploadFile(file, {
111+
// oxlint-disable-next-line max-params
112+
#uploadFile = async (
113+
client: BlockBlobClient,
114+
data: Blob | string | ReadableStream,
115+
mimeType: string,
116+
abortSignal?: AbortSignal,
117+
): Promise<void> => {
118+
if (typeof data === "string") {
119+
const blob = new Blob([data], { type: mimeType });
120+
await client.uploadData(blob, {
121+
abortSignal,
69122
blobHTTPHeaders: { blobContentType: mimeType },
70123
});
71124
return;
72125
}
73-
if (file instanceof Blob) {
74-
await client.uploadData(file, {
126+
if (data instanceof Blob) {
127+
await client.uploadData(data, {
128+
abortSignal,
75129
blobHTTPHeaders: { blobContentType: mimeType },
76130
});
77131
return;
78132
}
79-
if (file instanceof ReadableStream) {
80-
const stream = file as unknown as streamWeb.ReadableStream;
133+
if (data instanceof ReadableStream) {
134+
const stream = data as unknown as streamWeb.ReadableStream;
81135
await client.uploadStream(
82136
Readable.fromWeb(stream),
83137
undefined,
84138
undefined,
85-
{ blobHTTPHeaders: { blobContentType: mimeType } },
139+
{ abortSignal, blobHTTPHeaders: { blobContentType: mimeType } },
86140
);
87141
return;
88142
}
89143

90144
throw new Error(`Unknown file type`);
91145
};
92146

93-
uploadDir: StorageService["uploadDir"] = async (
94-
containerName,
95-
dirpath,
96-
destPrefix,
147+
hasFile: StorageService["hasFile"] = async (
148+
containerId,
149+
filepath,
150+
options,
97151
) => {
98-
const containerClient = this.#client.getContainerClient(containerName);
99-
100-
const files = fs
101-
.readdirSync(dirpath, {
102-
recursive: true,
103-
withFileTypes: true,
104-
})
105-
.filter((file) => file.isFile() && !file.name.startsWith("."))
106-
.map((file) => path.join(file.parentPath, file.name));
107-
108-
// context.info(`Found ${files.length} files in dir to upload: ${dirpath}.`);
109-
const uploadErrors = new Map<string, unknown>();
110-
111-
for (const filepath of files) {
112-
if (!fs.existsSync(filepath)) {
113-
// context.warn(`File ${filepath} does not exist, skipping.`);
114-
continue;
115-
}
116-
117-
let blobName = filepath.replace(`${dirpath}/`, "");
118-
if (destPrefix) {
119-
blobName = path.posix.join(destPrefix, blobName);
120-
}
121-
122-
try {
123-
// context.debug(`Uploading '${filepath}' to '${newFilepath}'...`);
124-
// oxlint-disable-next-line no-await-in-loop
125-
const response = await containerClient
126-
.getBlockBlobClient(blobName)
127-
.uploadFile(filepath);
128-
129-
if (response.errorCode) {
130-
throw response.errorCode;
131-
}
132-
} catch (error) {
133-
// context.error(
134-
// `Failed to upload blob '${blobName}'. Error: ${errorMessage}`,
135-
// );
136-
uploadErrors.set(blobName, error);
137-
}
138-
}
139-
140-
if (uploadErrors.size > 0) {
141-
throw new Error(
142-
`Failed to upload ${uploadErrors.size} files to container: ${containerClient.containerName}.`,
143-
);
144-
}
145-
146-
return;
152+
const containerClient = this.#client.getContainerClient(containerId);
153+
const blockBlobClient = containerClient.getBlockBlobClient(filepath);
154+
return await blockBlobClient.exists({ abortSignal: options.abortSignal });
147155
};
148156

149-
downloadFile = async (
150-
containerName: string,
151-
filepath: string,
152-
): Promise<ReadableStream> => {
153-
const containerClient = this.#client.getContainerClient(containerName);
157+
downloadFile: StorageService["downloadFile"] = async (
158+
containerId,
159+
filepath,
160+
options,
161+
) => {
162+
const containerClient = this.#client.getContainerClient(containerId);
154163
const blockBlobClient = containerClient.getBlockBlobClient(filepath);
155164

156165
if (!(await blockBlobClient.exists())) {
157166
throw new Error(
158-
`File '${filepath}' not found in container '${containerName}'.`,
167+
`File '${filepath}' not found in container '${containerId}'.`,
159168
);
160169
}
161170

162-
const downloadResponse = await blockBlobClient.download(0);
171+
const downloadResponse = await blockBlobClient.download(0, undefined, {
172+
abortSignal: options.abortSignal,
173+
});
163174

164175
if (!downloadResponse.readableStreamBody) {
165176
throw new Error(
166-
`File '${filepath}' in container '${containerName}' is not downloadable.`,
177+
`File '${filepath}' in container '${containerId}' is not downloadable.`,
167178
);
168179
}
169180

170-
const headers = new Headers();
171-
if (downloadResponse.contentType) {
172-
headers.set("Content-Type", downloadResponse.contentType);
173-
}
174-
if (downloadResponse.contentLength) {
175-
headers.set("Content-Length", downloadResponse.contentLength.toString());
176-
}
177-
if (downloadResponse.contentEncoding) {
178-
headers.set("Content-Encoding", downloadResponse.contentEncoding);
179-
}
180-
181-
return downloadResponse.readableStreamBody as unknown as ReadableStream;
181+
return {
182+
content: downloadResponse.readableStreamBody as unknown as ReadableStream,
183+
mimeType: downloadResponse.contentType,
184+
path: filepath,
185+
};
182186
};
183187
}

packages/adapter-azure/src/cosmosdb.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ export class AzureCosmosDatabaseService implements DatabaseService {
4040
return;
4141
};
4242

43+
hasCollection: DatabaseService["hasCollection"] = async (
44+
collectionId,
45+
options,
46+
) => {
47+
try {
48+
const response = await this.#db
49+
.container(collectionId)
50+
.read({ abortSignal: options.abortSignal });
51+
return !!response.resource;
52+
} catch {
53+
return false;
54+
}
55+
};
56+
4357
deleteCollection: DatabaseService["deleteCollection"] = async (
4458
collectionId,
4559
options,
@@ -89,6 +103,16 @@ export class AzureCosmosDatabaseService implements DatabaseService {
89103
return;
90104
};
91105

106+
hasDocument: DatabaseService["hasDocument"] = async (
107+
collectionId,
108+
documentId,
109+
options,
110+
) => {
111+
const item = this.#db.container(collectionId).item(documentId);
112+
const response = await item.read({ abortSignal: options.abortSignal });
113+
return !!response.resource;
114+
};
115+
92116
deleteDocument: DatabaseService["deleteDocument"] = async (
93117
collectionId,
94118
documentId,

0 commit comments

Comments
 (0)