1import { Actor } from 'apify';
2
3const RATE_LIMIT_DELAY = 1000;
4
5async function sleep(ms) {
6 return new Promise(resolve => setTimeout(resolve, ms));
7}
8
9async function pinFileByUrl(url, apiKey, network = 'public', privateExpiration = 86400) {
10 try {
11 console.log(`📌 Attempting to pin: ${url} (${network} network)`);
12
13
14 const fileResponse = await fetch(url);
15 if (!fileResponse.ok) {
16 throw new Error(`Failed to fetch file: ${fileResponse.status} ${fileResponse.statusText}`);
17 }
18
19 const blob = await fileResponse.blob();
20 const formData = new FormData();
21
22
23 const urlObj = new URL(url);
24 const pathname = urlObj.pathname;
25 const filename = pathname.split('/').pop() || 'file';
26
27 formData.append('file', blob, filename);
28 formData.append('network', network);
29 formData.append('name', filename);
30
31
32 const uploadResponse = await fetch('https://uploads.pinata.cloud/v3/files', {
33 method: 'POST',
34 headers: {
35 'Authorization': `Bearer ${apiKey}`
36 },
37 body: formData
38 });
39
40 if (!uploadResponse.ok) {
41 const errorText = await uploadResponse.text();
42 throw new Error(`Pinata upload failed: ${uploadResponse.status} - ${errorText}`);
43 }
44
45 const result = await uploadResponse.json();
46 console.log(`✅ Successfully pinned: ${url}`);
47
48 let web3Url;
49
50
51 if (network === 'private') {
52 console.log(`🔐 Generating signed URL for private file (expires in ${privateExpiration}s)...`);
53
54 const signedUrlResponse = await fetch('https://api.pinata.cloud/v3/files/private/download_link', {
55 method: 'POST',
56 headers: {
57 'Authorization': `Bearer ${apiKey}`,
58 'Content-Type': 'application/json'
59 },
60 body: JSON.stringify({
61 url: `https://os.mypinata.cloud/files/${result.data.cid}`,
62 expires: privateExpiration,
63 date: Math.floor(Date.now() / 1000),
64 method: 'GET'
65 })
66 });
67
68 if (!signedUrlResponse.ok) {
69 const errorText = await signedUrlResponse.text();
70 throw new Error(`Failed to generate signed URL: ${signedUrlResponse.status} - ${errorText}`);
71 }
72
73 const signedUrlResult = await signedUrlResponse.json();
74 web3Url = signedUrlResult.data;
75 console.log(`🔐 Signed URL generated (valid for ${privateExpiration}s)`);
76 } else {
77
78 web3Url = `https://gateway.pinata.cloud/ipfs/${result.data.cid}`;
79 }
80
81 return {
82 success: true,
83 cid: result.data.cid,
84 web3Url: web3Url
85 };
86
87 } catch (error) {
88 console.error(`❌ Failed to pin ${url}: ${error.message}`);
89 return {
90 success: false,
91 error: error.message
92 };
93 }
94}
95
96Actor.main(async () => {
97 console.log('🚀 Starting Upload to Web3...');
98
99 const input = await Actor.getInput();
100 const { startUrls, pinataApiKey, visibility = 'public', privateExpiration = 86400 } = input;
101
102 if (!startUrls || startUrls.length === 0) {
103 throw new Error('No URLs provided');
104 }
105
106 if (!pinataApiKey) {
107 throw new Error('Pinata API Key is required');
108 }
109
110 console.log(`📊 Processing ${startUrls.length} URLs`);
111
112
113 const dataset = await Actor.openDataset();
114
115 for (let i = 0; i < startUrls.length; i++) {
116 const urlEntry = startUrls[i];
117 const url = urlEntry.url;
118
119 console.log(`\n[${i + 1}/${startUrls.length}] Processing: ${url}`);
120
121 const result = await pinFileByUrl(url, pinataApiKey, visibility, privateExpiration);
122
123 const output = {
124 url: url,
125 cid: result.success ? result.cid : null,
126 web3Url: result.success ? result.web3Url : null,
127 status: result.success ? 'success' : `Error - ${result.error}`
128 };
129
130 await dataset.pushData(output);
131
132
133 if (i < startUrls.length - 1) {
134 console.log(`⏳ Waiting ${RATE_LIMIT_DELAY}ms for rate limiting...`);
135 await sleep(RATE_LIMIT_DELAY);
136 }
137 }
138
139 console.log('\n✅ Upload to Web3 completed!');
140});