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
19 changes: 14 additions & 5 deletions src/core/classes/uploads/UploadBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { PinataConfig, PinataMetadata, UploadOptions } from "../../types";
import type {
PinataConfig,
PinataMetadata,
UploadOptions,
CidVersion,
} from "../../types";

export class UploadBuilder<T> {
private config: PinataConfig | undefined;
Expand All @@ -15,6 +20,7 @@ export class UploadBuilder<T> {
private isStreamable: boolean | undefined;
private peerAddresses: string[] | undefined;
private carFormat: boolean | undefined;
private _cidVersion: CidVersion | undefined;

constructor(
config: PinataConfig | undefined,
Expand Down Expand Up @@ -61,10 +67,10 @@ export class UploadBuilder<T> {
return this;
}

// cidVersion(v: 0 | 1): UploadBuilder<T> {
// this.version = v;
// return this;
// }
cidVersion(v: CidVersion): UploadBuilder<T> {
this._cidVersion = v;
return this;
}

group(groupId: string): UploadBuilder<T> {
this.groupId = groupId;
Expand Down Expand Up @@ -121,6 +127,9 @@ export class UploadBuilder<T> {
if (this.carFormat) {
options.car = this.carFormat;
}
if (this._cidVersion !== undefined) {
options.cid_version = this._cidVersion;
}
this.args[this.args.length - 1] = options;
return this.uploadFunction(this.config, ...this.args).then(
onfulfilled,
Expand Down
26 changes: 22 additions & 4 deletions src/core/functions/uploads/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,25 @@ export const uploadFile = async (
metadata += `,car ${btoa("true")}`;
}

let updatedEndpoint: string = `${endpoint}/files`;
if (options?.cid_version !== undefined) {
metadata += `,cid_version ${btoa(options.cid_version)}`;
}

// Build URL with query parameters for chunked uploads
let updatedEndpoint: string = `${endpoint}/files`;
if (options?.url) {
updatedEndpoint = options.url;
}

const urlReq = await fetch(updatedEndpoint, {
const requestUrl = new URL(updatedEndpoint);
if (options?.cid_version !== undefined) {
requestUrl.searchParams.set(
"X-Upload-Option-Cid-Version",
options.cid_version.toString(),
);
}

const urlReq = await fetch(requestUrl.toString(), {
method: "POST",
headers: {
"Upload-Length": `${file.size}`,
Expand Down Expand Up @@ -98,7 +110,7 @@ export const uploadFile = async (

while (retryCount <= maxRetries) {
try {
uploadReq = await fetch(url as string, {
uploadReq = await fetch(url!, {
method: "PATCH",
headers: {
"Content-Type": "application/offset+octet-stream",
Expand Down Expand Up @@ -248,9 +260,15 @@ export const uploadFile = async (
data.append("car", "true");
}

if (options?.cid_version !== undefined) {
data.append("cid_version", options.cid_version.toString());
}

if (options?.url) {
try {
const request = await fetch(options.url, {
const url = new URL(options.url);

const request = await fetch(url.toString(), {
method: "POST",
headers: headers,
body: data,
Expand Down
10 changes: 10 additions & 0 deletions src/core/types/uploads.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { PinataMetadata } from "./";

export type CidVersion = "v0" | "v1";

export type UploadResponse = {
id: string;
name: string;
Expand All @@ -26,6 +28,10 @@ export type UploadOptions = {
streamable?: boolean;
peerAddresses?: string[];
car?: boolean;
/**
* CID version "v1" or "v0" (defaults to v1 if falsy)
*/
cid_version?: CidVersion;
};

export type SignedUploadUrlOptions = {
Expand All @@ -39,6 +45,10 @@ export type SignedUploadUrlOptions = {
mimeTypes?: string[];
streamable?: boolean;
car?: boolean;
/**
* CID version "v1" or "v0" (defaults to v1 if falsy)
*/
cid_version?: CidVersion;
};

export type UploadCIDOptions = {
Expand Down
175 changes: 175 additions & 0 deletions tests/uploads/file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
UploadOptions,
UploadResponse,
PinataMetadata,
CidVersion,
} from "../../src";
import {
PinataError,
Expand Down Expand Up @@ -203,4 +204,178 @@ describe("uploadFile function", () => {
}),
);
});

it("should upload file with CID version 0", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce({ data: mockResponse }),
});

const mockOptions: UploadOptions = {
cid_version: "v0" as CidVersion,
};

await uploadFile(mockConfig, mockFile, "public", mockOptions);

const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
const formData = fetchCall[1].body as FormData;

expect(formData.get("cid_version")).toBe("v0");
expect(global.fetch).toHaveBeenCalledWith(
"https://uploads.pinata.cloud/v3/files",
expect.objectContaining({
method: "POST",
headers: {
Source: "sdk/file",
Authorization: "Bearer test-jwt",
},
body: expect.any(FormData),
}),
);
});

it("should upload file with CID version 1", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce({ data: mockResponse }),
});

const mockOptions: UploadOptions = {
cid_version: "v1" as CidVersion,
};

await uploadFile(mockConfig, mockFile, "public", mockOptions);

const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
const formData = fetchCall[1].body as FormData;

expect(formData.get("cid_version")).toBe("v1");
expect(global.fetch).toHaveBeenCalledWith(
"https://uploads.pinata.cloud/v3/files",
expect.objectContaining({
method: "POST",
headers: {
Source: "sdk/file",
Authorization: "Bearer test-jwt",
},
body: expect.any(FormData),
}),
);
});

it("should upload same file with both CID versions", async () => {
const testFile = new File(["test content for cid versions"], "test.txt", {
type: "text/plain",
});

// Mock for CID version 0
global.fetch = jest
.fn()
.mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce({
data: { ...mockResponse, cid: "QmTestVersion0" },
}),
})
.mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce({
data: {
...mockResponse,
cid: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
},
}),
});

// Upload with CID version 0
const result0 = await uploadFile(mockConfig, testFile, "public", {
cid_version: "v0" as CidVersion,
});

// Upload with CID version 1
const result1 = await uploadFile(mockConfig, testFile, "public", {
cid_version: "v1" as CidVersion,
});

// Verify both uploads
expect(result0.cid).toBe("QmTestVersion0");
expect(result1.cid).toBe(
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
);

// Verify fetch was called twice
expect(global.fetch).toHaveBeenCalledTimes(2);

// Check CID version 0 FormData
const fetchCall0 = (global.fetch as jest.Mock).mock.calls[0];
const formData0 = fetchCall0[1].body as FormData;
expect(formData0.get("cid_version")).toBe("v0");

// Check CID version 1 FormData
const fetchCall1 = (global.fetch as jest.Mock).mock.calls[1];
const formData1 = fetchCall1[1].body as FormData;
expect(formData1.get("cid_version")).toBe("v1");
});

it("should upload file with CID version combined with other options", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce({ data: mockResponse }),
});

const mockOptions: UploadOptions = {
cid_version: "v1" as CidVersion,
car: true,
groupId: "test-group",
streamable: true,
};

await uploadFile(mockConfig, mockFile, "public", mockOptions);

const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
const formData = fetchCall[1].body as FormData;

expect(formData.get("cid_version")).toBe("v1");
expect(formData.get("car")).toBe("true");
expect(formData.get("group_id")).toBe("test-group");
expect(formData.get("streamable")).toBe("true");
});

it("should include CID version in metadata for large file upload", async () => {
// Create a large file (over 90MB) to trigger chunked upload metadata path
const largeFileSize = 94371841; // Just over the threshold
const largeFile = new File(
[new ArrayBuffer(largeFileSize)],
"large-test.txt",
{
type: "text/plain",
},
);

// Mock the first call (initial upload request) to verify metadata
global.fetch = jest.fn().mockImplementationOnce(() => {
// We're just testing that the request is made with correct metadata
// Return a promise that we can inspect the call and then reject to avoid complex mocking
return Promise.reject(new Error("Test stopped after metadata check"));
});

const mockOptions: UploadOptions = {
cid_version: "v1" as CidVersion,
};

try {
await uploadFile(mockConfig, largeFile, "private", mockOptions);
} catch (error) {
// Expected to fail, we just want to check the metadata
}

// Check that the initial request contains cid_version in Upload-Metadata
expect(global.fetch).toHaveBeenCalledTimes(1);
const initialCall = (global.fetch as jest.Mock).mock.calls[0];
const uploadMetadata = initialCall[1].headers["Upload-Metadata"];

// The metadata should contain base64 encoded cid_version
expect(uploadMetadata).toContain("cid_version");
expect(uploadMetadata).toContain(btoa("v1"));
});
});