Skip to content

Commit e39c383

Browse files
authored
Merge pull request #26 from ethersphere/NFT_solution
feat: upload NFT files all in one go
2 parents 2dc8464 + 935d03a commit e39c383

3 files changed

Lines changed: 589 additions & 0 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import JSZip from 'jszip';
2+
import Tar from 'tar-js';
3+
4+
export interface NFTCollectionResult {
5+
imagesReference: string;
6+
metadataReference: string;
7+
totalImages: number;
8+
totalMetadata: number;
9+
}
10+
11+
export interface NFTCollectionProcessorParams {
12+
zipFile: File;
13+
postageBatchId: string;
14+
walletClient: any;
15+
publicClient: any;
16+
address: `0x${string}` | undefined;
17+
beeApiUrl: string;
18+
setProgress: (progress: number) => void;
19+
setStatusMessage: (message: string) => void;
20+
}
21+
22+
/**
23+
* Process NFT collection ZIP file and upload both images and metadata
24+
*/
25+
export const processNFTCollection = async (
26+
params: NFTCollectionProcessorParams
27+
): Promise<NFTCollectionResult> => {
28+
const {
29+
zipFile,
30+
postageBatchId,
31+
walletClient,
32+
publicClient,
33+
address,
34+
beeApiUrl,
35+
setProgress,
36+
setStatusMessage,
37+
} = params;
38+
39+
setStatusMessage('Extracting NFT collection...');
40+
setProgress(10);
41+
42+
// Load and extract ZIP file
43+
const jszip = new JSZip();
44+
const zipContents = await jszip.loadAsync(zipFile);
45+
46+
// Separate images and JSON files
47+
const imageFiles: { [key: string]: Uint8Array } = {};
48+
const jsonFiles: { [key: string]: string } = {};
49+
50+
// Process all files in the ZIP
51+
for (const [filename, zipEntry] of Object.entries(zipContents.files)) {
52+
if (zipEntry.dir) continue; // Skip directories
53+
54+
// Determine if file is in images or json folder
55+
const pathParts = filename.split('/');
56+
if (pathParts.length < 2) continue; // Skip files not in folders
57+
58+
const folderName = pathParts[0].toLowerCase();
59+
const fileName = pathParts[pathParts.length - 1]; // Get just the filename
60+
61+
if (folderName === 'images') {
62+
// Process image files
63+
const content = await zipEntry.async('arraybuffer');
64+
imageFiles[fileName] = new Uint8Array(content);
65+
} else if (folderName === 'json') {
66+
// Process JSON files
67+
const content = await zipEntry.async('string');
68+
jsonFiles[fileName] = content;
69+
}
70+
}
71+
72+
console.log(
73+
`Found ${Object.keys(imageFiles).length} images and ${Object.keys(jsonFiles).length} JSON files`
74+
);
75+
76+
if (Object.keys(imageFiles).length === 0) {
77+
throw new Error('No images found in the images folder');
78+
}
79+
80+
if (Object.keys(jsonFiles).length === 0) {
81+
throw new Error('No JSON metadata files found in the json folder');
82+
}
83+
84+
// Step 1: Create TAR with images (without subfolder)
85+
setStatusMessage('Creating images archive...');
86+
setProgress(20);
87+
88+
const imagesTar = new Tar();
89+
Object.entries(imageFiles).forEach(([filename, content]) => {
90+
imagesTar.append(filename, content);
91+
});
92+
93+
const imagesTarFile = new File([imagesTar.out], 'images.tar', {
94+
type: 'application/x-tar',
95+
lastModified: new Date().getTime(),
96+
});
97+
98+
// Step 2: Upload images TAR
99+
setStatusMessage('Uploading images...');
100+
setProgress(30);
101+
102+
const imagesReference = await uploadFile(imagesTarFile, {
103+
postageBatchId,
104+
walletClient,
105+
publicClient,
106+
address,
107+
beeApiUrl,
108+
});
109+
110+
console.log('Images uploaded with reference:', imagesReference);
111+
112+
// Step 3: Modify JSON files to use the bzz.link URL format
113+
setStatusMessage('Processing metadata...');
114+
setProgress(60);
115+
116+
const modifiedJsonFiles: { [key: string]: string } = {};
117+
118+
Object.entries(jsonFiles).forEach(([filename, content]) => {
119+
try {
120+
const metadata = JSON.parse(content);
121+
122+
// Look for image property and update it
123+
if (metadata.image) {
124+
// Extract the image filename from the original image path
125+
const originalImagePath = metadata.image;
126+
let imageName = originalImagePath;
127+
128+
// Handle various formats of image references
129+
if (originalImagePath.includes('/')) {
130+
imageName = originalImagePath.split('/').pop();
131+
}
132+
133+
// Create the new bzz.link URL
134+
metadata.image = `https://bzz.link/bzz/${imagesReference}/${imageName}`;
135+
}
136+
137+
// Handle other common NFT metadata image fields
138+
if (metadata.image_url) {
139+
const originalImagePath = metadata.image_url;
140+
let imageName = originalImagePath;
141+
if (originalImagePath.includes('/')) {
142+
imageName = originalImagePath.split('/').pop();
143+
}
144+
metadata.image_url = `https://bzz.link/bzz/${imagesReference}/${imageName}`;
145+
}
146+
147+
modifiedJsonFiles[filename] = JSON.stringify(metadata, null, 2);
148+
} catch (error) {
149+
console.error(`Error processing JSON file ${filename}:`, error);
150+
// Keep original content if parsing fails
151+
modifiedJsonFiles[filename] = content;
152+
}
153+
});
154+
155+
// Step 4: Create TAR with modified JSON files (without subfolder)
156+
setStatusMessage('Creating metadata archive...');
157+
setProgress(80);
158+
159+
const metadataTar = new Tar();
160+
Object.entries(modifiedJsonFiles).forEach(([filename, content]) => {
161+
metadataTar.append(filename, new TextEncoder().encode(content));
162+
});
163+
164+
const metadataTarFile = new File([metadataTar.out], 'metadata.tar', {
165+
type: 'application/x-tar',
166+
lastModified: new Date().getTime(),
167+
});
168+
169+
// Step 5: Upload metadata TAR
170+
setStatusMessage('Uploading metadata...');
171+
setProgress(90);
172+
173+
const metadataReference = await uploadFile(metadataTarFile, {
174+
postageBatchId,
175+
walletClient,
176+
publicClient,
177+
address,
178+
beeApiUrl,
179+
});
180+
181+
console.log('Metadata uploaded with reference:', metadataReference);
182+
183+
setProgress(100);
184+
setStatusMessage('NFT collection upload complete!');
185+
186+
return {
187+
imagesReference,
188+
metadataReference,
189+
totalImages: Object.keys(imageFiles).length,
190+
totalMetadata: Object.keys(jsonFiles).length,
191+
};
192+
};
193+
194+
/**
195+
* Upload a file to Swarm
196+
*/
197+
const uploadFile = async (
198+
file: File,
199+
params: {
200+
postageBatchId: string;
201+
walletClient: any;
202+
publicClient: any;
203+
address: `0x${string}` | undefined;
204+
beeApiUrl: string;
205+
}
206+
): Promise<string> => {
207+
const { postageBatchId, walletClient, address, beeApiUrl } = params;
208+
209+
const isLocalhost = beeApiUrl.includes('localhost') || beeApiUrl.includes('127.0.0.1');
210+
211+
// Sign message for upload authorization
212+
const messageToSign = `${file.name}:${postageBatchId}`;
213+
const signedMessage = await walletClient.signMessage({
214+
message: messageToSign,
215+
});
216+
217+
// Setup headers
218+
const headers: Record<string, string> = {
219+
'Content-Type': 'application/x-tar',
220+
'swarm-postage-batch-id': postageBatchId,
221+
'swarm-pin': 'false',
222+
'swarm-deferred-upload': 'false',
223+
'swarm-collection': 'true',
224+
};
225+
226+
if (!isLocalhost) {
227+
headers['x-upload-signed-message'] = signedMessage;
228+
headers['x-uploader-address'] = address as string;
229+
headers['x-file-name'] = file.name;
230+
headers['x-message-content'] = messageToSign;
231+
}
232+
233+
// Upload using XMLHttpRequest for better control
234+
return new Promise((resolve, reject) => {
235+
const xhr = new XMLHttpRequest();
236+
const url = `${beeApiUrl}/bzz?name=${encodeURIComponent(file.name)}`;
237+
238+
xhr.open('POST', url);
239+
xhr.timeout = 10 * 60 * 1000; // 10 minute timeout
240+
241+
Object.entries(headers).forEach(([key, value]) => {
242+
xhr.setRequestHeader(key, value);
243+
});
244+
245+
xhr.onload = () => {
246+
if (xhr.status >= 200 && xhr.status < 300) {
247+
try {
248+
const response = JSON.parse(xhr.responseText);
249+
resolve(response.reference);
250+
} catch (error) {
251+
reject(new Error('Invalid response format'));
252+
}
253+
} else {
254+
reject(new Error(`Upload failed with status ${xhr.status}`));
255+
}
256+
};
257+
258+
xhr.onerror = () => {
259+
reject(new Error('Network error during upload'));
260+
};
261+
262+
xhr.ontimeout = () => {
263+
reject(new Error('Upload timeout'));
264+
};
265+
266+
xhr.send(file);
267+
});
268+
};

0 commit comments

Comments
 (0)