Skip to content

Commit 990d0f4

Browse files
Merge pull request #20 from yepcode/feat/storage-signed-urls
Add createSignedUrl to storage API
2 parents fdc23fc + a88e80d commit 990d0f4

6 files changed

Lines changed: 194 additions & 6 deletions

File tree

README.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const processes = await api.getProcesses();
107107

108108
### 6. Storage Objects
109109

110-
You can manage files in your YepCode workspace using the `YepCodeStorage` class. This allows you to upload, list, download, and delete files easily.
110+
You can manage files in your YepCode workspace using the `YepCodeStorage` class. This allows you to upload, list, download, delete, and generate signed URLs for files easily.
111111

112112
```js
113113
const { YepCodeStorage } = require('@yepcode/run');
@@ -126,6 +126,12 @@ console.log(files);
126126
const stream = await storage.download('path/myfile.txt');
127127
stream.pipe(fs.createWriteStream('./downloaded.txt'));
128128

129+
// Create a temporary signed URL (1 hour by default)
130+
const signedUrl = await storage.createSignedUrl('path/myfile.txt', {
131+
expiresInSeconds: 300 // Optional
132+
});
133+
console.log(signedUrl.url, signedUrl.expiresAt);
134+
129135
// Delete a file
130136
await storage.delete('myfile.txt');
131137
```
@@ -392,9 +398,33 @@ interface Process {
392398
}
393399
```
394400

401+
##### `createSignedUrl(data: CreateSignedUrlInput): Promise<SignedUrl>`
402+
403+
Creates a temporary signed URL for a stored file.
404+
405+
**Parameters:**
406+
407+
- `data.path`: Storage path (filename) to generate a signed URL for
408+
- `data.expiresInSeconds`: Optional expiry in seconds
409+
410+
**Returns:** Promise<SignedUrl>
411+
412+
```typescript
413+
interface CreateSignedUrlInput {
414+
path: string;
415+
expiresInSeconds?: number;
416+
}
417+
418+
interface SignedUrl {
419+
url: string;
420+
path: string;
421+
expiresAt: string;
422+
}
423+
```
424+
395425
### YepCodeStorage
396426

397-
Manages file storage in your YepCode workspace. Allows you to upload, list, download, and delete files using the YepCode API.
427+
Manages file storage in your YepCode workspace. Allows you to upload, list, download, delete, and generate signed URLs using the YepCode API.
398428

399429
#### Constructor
400430

@@ -434,6 +464,14 @@ Deletes a file from YepCode storage.
434464
- `filename`: Name of the file to delete
435465
- **Returns:** Promise<void>
436466

467+
##### `createSignedUrl(filename: string, options?: { expiresInSeconds?: number }): Promise<SignedUrl>`
468+
469+
Creates a temporary signed URL for a file in storage.
470+
471+
- `filename`: Name of the file to generate a signed URL for
472+
- `options.expiresInSeconds`: Optional expiry in seconds
473+
- **Returns:** Promise<SignedUrl>
474+
437475
##### `StorageObject`
438476

439477
```typescript
@@ -446,6 +484,16 @@ interface StorageObject {
446484
}
447485
```
448486

487+
##### `SignedUrl`
488+
489+
```typescript
490+
interface SignedUrl {
491+
url: string;
492+
path: string;
493+
expiresAt: string;
494+
}
495+
```
496+
449497
## License
450498

451499
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

src/api/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,17 @@ export type CreateStorageObjectInput = {
463463
file: File | Blob | Readable;
464464
};
465465

466+
export type CreateSignedUrlInput = {
467+
path: string;
468+
expiresInSeconds?: number;
469+
};
470+
471+
export type SignedUrl = {
472+
url: string;
473+
path: string;
474+
expiresAt: string;
475+
};
476+
466477
/**
467478
* Auth
468479
*/

src/api/yepcodeApi.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
VersionedModuleAliasesPaginatedResult,
3535
StorageObject,
3636
CreateStorageObjectInput,
37+
CreateSignedUrlInput,
38+
SignedUrl,
3739
Token,
3840
Sandbox,
3941
CreateSandboxInput,
@@ -796,6 +798,10 @@ export class YepCodeApi {
796798
);
797799
}
798800

801+
async createSignedUrl(data: CreateSignedUrlInput): Promise<SignedUrl> {
802+
return this.request("POST", "/storage/signed-urls", { data });
803+
}
804+
799805
// Auth endpoints
800806
async getToken(apiToken: string): Promise<Token> {
801807
return this.request("POST", "/auth/token", {

src/storage/yepcodeStorage.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { YepCodeApi } from "../api";
2-
import { StorageObject, YepCodeApiConfig } from "../api/types";
2+
import { SignedUrl, StorageObject, YepCodeApiConfig } from "../api/types";
33
import { Readable } from "stream";
44

55
export class YepCodeStorage {
@@ -27,4 +27,11 @@ export class YepCodeStorage {
2727
async delete(filename: string): Promise<void> {
2828
await this.api.deleteObject(filename);
2929
}
30+
31+
async createSignedUrl(
32+
filename: string,
33+
options: { expiresInSeconds?: number } = {}
34+
): Promise<SignedUrl> {
35+
return this.api.createSignedUrl({ path: filename, ...options });
36+
}
3037
}

tests/api/yepcodeApi.storage.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { YepCodeApi } from "../../src/api/yepcodeApi";
2-
import { StorageObject } from "../../src/api/types";
1+
import { YepCodeApi, YepCodeApiError } from "../../src/api/yepcodeApi";
2+
import { SignedUrl, StorageObject } from "../../src/api/types";
33
import fs, { createWriteStream, readFileSync } from "fs";
44
import path from "path";
55
import { Readable } from "stream";
@@ -113,4 +113,68 @@ describe.skip("YepCodeApi", () => {
113113
await verifyDownloadedFile(result, downloadedFile, testFilePath);
114114
});
115115
});
116+
117+
describe("createSignedUrl", () => {
118+
it("should return a signed url with the default expiry", async () => {
119+
const file: File = new File([readFileSync(testFilePath)], testName);
120+
await api.createObject({ name: testName, file });
121+
122+
const result: SignedUrl = await api.createSignedUrl({ path: testName });
123+
124+
expect(typeof result.url).toBe("string");
125+
expect(result.url.length).toBeGreaterThan(0);
126+
expect(result.path).toBe(testName);
127+
128+
const expiresAt = new Date(result.expiresAt).getTime();
129+
const expectedExpiry = Date.now() + 3600 * 1000;
130+
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(60 * 1000);
131+
});
132+
133+
it("should return a signed url with a custom expiry", async () => {
134+
const file: File = new File([readFileSync(testFilePath)], testName);
135+
await api.createObject({ name: testName, file });
136+
137+
const result: SignedUrl = await api.createSignedUrl({
138+
path: testName,
139+
expiresInSeconds: 60,
140+
});
141+
142+
const expiresAt = new Date(result.expiresAt).getTime();
143+
const expectedExpiry = Date.now() + 60 * 1000;
144+
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(30 * 1000);
145+
});
146+
147+
it("should return content matching the original file when fetched", async () => {
148+
const file: File = new File([readFileSync(testFilePath)], testName);
149+
await api.createObject({ name: testName, file });
150+
151+
const { url } = await api.createSignedUrl({ path: testName });
152+
153+
const response = await fetch(url);
154+
expect(response.ok).toBe(true);
155+
const body = await response.text();
156+
expect(body).toBe(readFileSync(testFilePath, "utf8"));
157+
});
158+
159+
it("should throw a 404 when the file does not exist", async () => {
160+
await expect(
161+
api.createSignedUrl({ path: "does-not-exist.txt" })
162+
).rejects.toMatchObject({
163+
name: "YepCodeApiError",
164+
status: 404,
165+
} as Partial<YepCodeApiError>);
166+
});
167+
168+
it("should throw a 400 when expiresInSeconds is out of range", async () => {
169+
const file: File = new File([readFileSync(testFilePath)], testName);
170+
await api.createObject({ name: testName, file });
171+
172+
await expect(
173+
api.createSignedUrl({ path: testName, expiresInSeconds: 999999 })
174+
).rejects.toMatchObject({
175+
name: "YepCodeApiError",
176+
status: 400,
177+
} as Partial<YepCodeApiError>);
178+
});
179+
});
116180
});

tests/storage/yepcodeStorage.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { YepCodeStorage } from "../../src/storage";
2-
import { StorageObject } from "../../src/api/types";
2+
import { YepCodeApiError } from "../../src/api/yepcodeApi";
3+
import { SignedUrl, StorageObject } from "../../src/api/types";
34
import fs, { createWriteStream, readFileSync } from "fs";
45
import path from "path";
56
import { Readable } from "stream";
@@ -117,4 +118,55 @@ describe.skip("YepCodeStorage", () => {
117118
await verifyDownloadedFile(result, downloadedFile, testFilePath);
118119
});
119120
});
121+
122+
describe("createSignedUrl", () => {
123+
it("should return a signed url with the default expiry", async () => {
124+
const file: File = new File([readFileSync(testFilePath)], testName);
125+
await storage.upload(testName, file);
126+
127+
const result: SignedUrl = await storage.createSignedUrl(testName);
128+
129+
expect(typeof result.url).toBe("string");
130+
expect(result.url.length).toBeGreaterThan(0);
131+
expect(result.path).toBe(testName);
132+
133+
const expiresAt = new Date(result.expiresAt).getTime();
134+
const expectedExpiry = Date.now() + 3600 * 1000;
135+
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(60 * 1000);
136+
});
137+
138+
it("should return a signed url with a custom expiry", async () => {
139+
const file: File = new File([readFileSync(testFilePath)], testName);
140+
await storage.upload(testName, file);
141+
142+
const result: SignedUrl = await storage.createSignedUrl(testName, {
143+
expiresInSeconds: 60,
144+
});
145+
146+
const expiresAt = new Date(result.expiresAt).getTime();
147+
const expectedExpiry = Date.now() + 60 * 1000;
148+
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(30 * 1000);
149+
});
150+
151+
it("should throw a 404 when the file does not exist", async () => {
152+
await expect(
153+
storage.createSignedUrl("does-not-exist.txt")
154+
).rejects.toMatchObject({
155+
name: "YepCodeApiError",
156+
status: 404,
157+
} as Partial<YepCodeApiError>);
158+
});
159+
160+
it("should throw a 400 when expiresInSeconds is out of range", async () => {
161+
const file: File = new File([readFileSync(testFilePath)], testName);
162+
await storage.upload(testName, file);
163+
164+
await expect(
165+
storage.createSignedUrl(testName, { expiresInSeconds: 999999 })
166+
).rejects.toMatchObject({
167+
name: "YepCodeApiError",
168+
status: 400,
169+
} as Partial<YepCodeApiError>);
170+
});
171+
});
120172
});

0 commit comments

Comments
 (0)