Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const processes = await api.getProcesses();

### 6. Storage Objects

You can manage files in your YepCode workspace using the `YepCodeStorage` class. This allows you to upload, list, download, and delete files easily.
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.

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

// Create a temporary signed URL (1 hour by default)
const signedUrl = await storage.createSignedUrl('path/myfile.txt', {
expiresInSeconds: 300 // Optional
});
console.log(signedUrl.url, signedUrl.expiresAt);

// Delete a file
await storage.delete('myfile.txt');
```
Expand Down Expand Up @@ -392,9 +398,33 @@ interface Process {
}
```

##### `createSignedUrl(data: CreateSignedUrlInput): Promise<SignedUrl>`

Creates a temporary signed URL for a stored file.

**Parameters:**

- `data.path`: Storage path (filename) to generate a signed URL for
- `data.expiresInSeconds`: Optional expiry in seconds

**Returns:** Promise<SignedUrl>

```typescript
interface CreateSignedUrlInput {
path: string;
expiresInSeconds?: number;
}

interface SignedUrl {
url: string;
path: string;
expiresAt: string;
}
```

### YepCodeStorage

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

#### Constructor

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

##### `createSignedUrl(filename: string, options?: { expiresInSeconds?: number }): Promise<SignedUrl>`

Creates a temporary signed URL for a file in storage.

- `filename`: Name of the file to generate a signed URL for
- `options.expiresInSeconds`: Optional expiry in seconds
- **Returns:** Promise<SignedUrl>

##### `StorageObject`

```typescript
Expand All @@ -446,6 +484,16 @@ interface StorageObject {
}
```

##### `SignedUrl`

```typescript
interface SignedUrl {
url: string;
path: string;
expiresAt: string;
}
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
11 changes: 11 additions & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,17 @@ export type CreateStorageObjectInput = {
file: File | Blob | Readable;
};

export type CreateSignedUrlInput = {
path: string;
expiresInSeconds?: number;
};

export type SignedUrl = {
url: string;
path: string;
expiresAt: string;
};

/**
* Auth
*/
Expand Down
6 changes: 6 additions & 0 deletions src/api/yepcodeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
VersionedModuleAliasesPaginatedResult,
StorageObject,
CreateStorageObjectInput,
CreateSignedUrlInput,
SignedUrl,
Token,
Sandbox,
CreateSandboxInput,
Expand Down Expand Up @@ -796,6 +798,10 @@ export class YepCodeApi {
);
}

async createSignedUrl(data: CreateSignedUrlInput): Promise<SignedUrl> {
return this.request("POST", "/storage/signed-urls", { data });
}

// Auth endpoints
async getToken(apiToken: string): Promise<Token> {
return this.request("POST", "/auth/token", {
Expand Down
9 changes: 8 additions & 1 deletion src/storage/yepcodeStorage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { YepCodeApi } from "../api";
import { StorageObject, YepCodeApiConfig } from "../api/types";
import { SignedUrl, StorageObject, YepCodeApiConfig } from "../api/types";
import { Readable } from "stream";

export class YepCodeStorage {
Expand Down Expand Up @@ -27,4 +27,11 @@ export class YepCodeStorage {
async delete(filename: string): Promise<void> {
await this.api.deleteObject(filename);
}

async createSignedUrl(
filename: string,
options: { expiresInSeconds?: number } = {}
): Promise<SignedUrl> {
return this.api.createSignedUrl({ path: filename, ...options });
}
}
68 changes: 66 additions & 2 deletions tests/api/yepcodeApi.storage.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { YepCodeApi } from "../../src/api/yepcodeApi";
import { StorageObject } from "../../src/api/types";
import { YepCodeApi, YepCodeApiError } from "../../src/api/yepcodeApi";
import { SignedUrl, StorageObject } from "../../src/api/types";
import fs, { createWriteStream, readFileSync } from "fs";
import path from "path";
import { Readable } from "stream";
Expand Down Expand Up @@ -113,4 +113,68 @@ describe.skip("YepCodeApi", () => {
await verifyDownloadedFile(result, downloadedFile, testFilePath);
});
});

describe("createSignedUrl", () => {
it("should return a signed url with the default expiry", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await api.createObject({ name: testName, file });

const result: SignedUrl = await api.createSignedUrl({ path: testName });

expect(typeof result.url).toBe("string");
expect(result.url.length).toBeGreaterThan(0);
expect(result.path).toBe(testName);

const expiresAt = new Date(result.expiresAt).getTime();
const expectedExpiry = Date.now() + 3600 * 1000;
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(60 * 1000);
});

it("should return a signed url with a custom expiry", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await api.createObject({ name: testName, file });

const result: SignedUrl = await api.createSignedUrl({
path: testName,
expiresInSeconds: 60,
});

const expiresAt = new Date(result.expiresAt).getTime();
const expectedExpiry = Date.now() + 60 * 1000;
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(30 * 1000);
});

it("should return content matching the original file when fetched", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await api.createObject({ name: testName, file });

const { url } = await api.createSignedUrl({ path: testName });

const response = await fetch(url);
expect(response.ok).toBe(true);
const body = await response.text();
expect(body).toBe(readFileSync(testFilePath, "utf8"));
});

it("should throw a 404 when the file does not exist", async () => {
await expect(
api.createSignedUrl({ path: "does-not-exist.txt" })
).rejects.toMatchObject({
name: "YepCodeApiError",
status: 404,
} as Partial<YepCodeApiError>);
});

it("should throw a 400 when expiresInSeconds is out of range", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await api.createObject({ name: testName, file });

await expect(
api.createSignedUrl({ path: testName, expiresInSeconds: 999999 })
).rejects.toMatchObject({
name: "YepCodeApiError",
status: 400,
} as Partial<YepCodeApiError>);
});
});
});
54 changes: 53 additions & 1 deletion tests/storage/yepcodeStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { YepCodeStorage } from "../../src/storage";
import { StorageObject } from "../../src/api/types";
import { YepCodeApiError } from "../../src/api/yepcodeApi";
import { SignedUrl, StorageObject } from "../../src/api/types";
import fs, { createWriteStream, readFileSync } from "fs";
import path from "path";
import { Readable } from "stream";
Expand Down Expand Up @@ -117,4 +118,55 @@ describe.skip("YepCodeStorage", () => {
await verifyDownloadedFile(result, downloadedFile, testFilePath);
});
});

describe("createSignedUrl", () => {
it("should return a signed url with the default expiry", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await storage.upload(testName, file);

const result: SignedUrl = await storage.createSignedUrl(testName);

expect(typeof result.url).toBe("string");
expect(result.url.length).toBeGreaterThan(0);
expect(result.path).toBe(testName);

const expiresAt = new Date(result.expiresAt).getTime();
const expectedExpiry = Date.now() + 3600 * 1000;
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(60 * 1000);
});

it("should return a signed url with a custom expiry", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await storage.upload(testName, file);

const result: SignedUrl = await storage.createSignedUrl(testName, {
expiresInSeconds: 60,
});

const expiresAt = new Date(result.expiresAt).getTime();
const expectedExpiry = Date.now() + 60 * 1000;
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(30 * 1000);
});

it("should throw a 404 when the file does not exist", async () => {
await expect(
storage.createSignedUrl("does-not-exist.txt")
).rejects.toMatchObject({
name: "YepCodeApiError",
status: 404,
} as Partial<YepCodeApiError>);
});

it("should throw a 400 when expiresInSeconds is out of range", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await storage.upload(testName, file);

await expect(
storage.createSignedUrl(testName, { expiresInSeconds: 999999 })
).rejects.toMatchObject({
name: "YepCodeApiError",
status: 400,
} as Partial<YepCodeApiError>);
});
});
});
Loading