Download all files from Rork projects as a ZIP archive with folder structure preserved
Size
10.2 KB
Version
1.0.1
Created
Dec 24, 2025
Updated
3 months ago
1// ==UserScript==
2// @name Rork Project Downloader
3// @description Download all files from Rork projects as a ZIP archive with folder structure preserved
4// @version 1.0.1
5// @match https://*.rork.com/*
6// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
7// @run-at document-start
8// @grant GM.xmlhttpRequest
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('Rork Project Downloader: Extension loaded');
14
15 // Store the captured snapshot data
16 let capturedSnapshot = null;
17 let projectName = 'rork-project';
18
19 // Intercept fetch requests to capture snapshot data
20 const originalFetch = unsafeWindow.fetch;
21 unsafeWindow.fetch = async function(...args) {
22 const response = await originalFetch.apply(this, args);
23
24 // Clone the response so we can read it without consuming it
25 const clonedResponse = response.clone();
26
27 try {
28 const url = args[0];
29 if (typeof url === 'string' && url.includes('/api/') && url.includes('snapshot')) {
30 console.log('Rork Project Downloader: Intercepted snapshot API call:', url);
31 const data = await clonedResponse.json();
32
33 if (data && (data.snapshot || data.files || data.tree)) {
34 console.log('Rork Project Downloader: Captured snapshot data');
35 capturedSnapshot = data;
36
37 // Try to extract project name from URL or data
38 const pathMatch = window.location.pathname.match(/\/p\/([^\/]+)/);
39 if (pathMatch) {
40 projectName = pathMatch[1];
41 }
42 if (data.name) {
43 projectName = data.name;
44 }
45 }
46 }
47 } catch (e) {
48 // Ignore JSON parse errors for non-JSON responses
49 }
50
51 return response;
52 };
53
54 // Also intercept XMLHttpRequest
55 const originalXHROpen = unsafeWindow.XMLHttpRequest.prototype.open;
56 const originalXHRSend = unsafeWindow.XMLHttpRequest.prototype.send;
57
58 unsafeWindow.XMLHttpRequest.prototype.open = function(method, url, ...rest) {
59 this._url = url;
60 return originalXHROpen.apply(this, [method, url, ...rest]);
61 };
62
63 unsafeWindow.XMLHttpRequest.prototype.send = function(...args) {
64 this.addEventListener('load', function() {
65 try {
66 if (this._url && this._url.includes('/api/') && this._url.includes('snapshot')) {
67 console.log('Rork Project Downloader: Intercepted XHR snapshot call:', this._url);
68 const data = JSON.parse(this.responseText);
69
70 if (data && (data.snapshot || data.files || data.tree)) {
71 console.log('Rork Project Downloader: Captured snapshot data via XHR');
72 capturedSnapshot = data;
73
74 const pathMatch = window.location.pathname.match(/\/p\/([^\/]+)/);
75 if (pathMatch) {
76 projectName = pathMatch[1];
77 }
78 if (data.name) {
79 projectName = data.name;
80 }
81 }
82 }
83 } catch (e) {
84 // Ignore errors
85 }
86 });
87 return originalXHRSend.apply(this, args);
88 };
89
90 // Function to extract files from snapshot data
91 function extractFilesFromSnapshot(snapshot) {
92 const files = [];
93
94 // Handle different snapshot data structures
95 if (snapshot.snapshot && snapshot.snapshot.files) {
96 // Structure: { snapshot: { files: { "path/to/file": { content: "..." } } } }
97 const snapshotFiles = snapshot.snapshot.files;
98 for (const [path, fileData] of Object.entries(snapshotFiles)) {
99 files.push({
100 path: path,
101 content: fileData.content || fileData.code || fileData
102 });
103 }
104 } else if (snapshot.files) {
105 // Structure: { files: { "path/to/file": "content" } } or { files: [{ path, content }] }
106 if (Array.isArray(snapshot.files)) {
107 files.push(...snapshot.files);
108 } else {
109 for (const [path, content] of Object.entries(snapshot.files)) {
110 files.push({
111 path: path,
112 content: typeof content === 'string' ? content : (content.content || content.code || JSON.stringify(content))
113 });
114 }
115 }
116 } else if (snapshot.tree) {
117 // Structure: { tree: { ... nested structure ... } }
118 function traverseTree(node, currentPath = '') {
119 if (node.type === 'file' && node.content) {
120 files.push({
121 path: currentPath,
122 content: node.content
123 });
124 } else if (node.children) {
125 for (const [name, child] of Object.entries(node.children)) {
126 const newPath = currentPath ? `${currentPath}/${name}` : name;
127 traverseTree(child, newPath);
128 }
129 }
130 }
131 traverseTree(snapshot.tree);
132 }
133
134 console.log(`Rork Project Downloader: Extracted ${files.length} files`);
135 return files;
136 }
137
138 // Function to create and download ZIP file
139 async function downloadProjectAsZip() {
140 if (!capturedSnapshot) {
141 alert('No project data captured yet. Please wait for the page to fully load or refresh the page.');
142 return;
143 }
144
145 try {
146 console.log('Rork Project Downloader: Creating ZIP file...');
147 const zip = new JSZip();
148
149 const files = extractFilesFromSnapshot(capturedSnapshot);
150
151 if (files.length === 0) {
152 alert('No files found in the captured snapshot. The data structure might have changed.');
153 console.error('Snapshot data:', capturedSnapshot);
154 return;
155 }
156
157 // Add each file to the ZIP with its folder structure
158 for (const file of files) {
159 if (file.path && file.content) {
160 zip.file(file.path, file.content);
161 console.log(`Added file: ${file.path}`);
162 }
163 }
164
165 // Generate the ZIP file
166 console.log('Rork Project Downloader: Generating ZIP blob...');
167 const blob = await zip.generateAsync({ type: 'blob' });
168
169 // Create download link
170 const url = URL.createObjectURL(blob);
171 const a = document.createElement('a');
172 a.href = url;
173 a.download = `${projectName}.zip`;
174 document.body.appendChild(a);
175 a.click();
176 document.body.removeChild(a);
177 URL.revokeObjectURL(url);
178
179 console.log('Rork Project Downloader: Download initiated successfully!');
180 alert(`Successfully downloaded ${files.length} files as ${projectName}.zip`);
181 } catch (error) {
182 console.error('Rork Project Downloader: Error creating ZIP:', error);
183 alert('Error creating ZIP file: ' + error.message);
184 }
185 }
186
187 // Function to add download button to the page
188 function addDownloadButton() {
189 // Check if we're on a project page
190 if (!window.location.pathname.includes('/p/')) {
191 return;
192 }
193
194 // Check if button already exists
195 if (document.getElementById('rork-download-btn')) {
196 return;
197 }
198
199 console.log('Rork Project Downloader: Adding download button...');
200
201 // Create download button
202 const button = document.createElement('button');
203 button.id = 'rork-download-btn';
204 button.textContent = '📥 Download Project';
205 button.style.cssText = `
206 position: fixed;
207 bottom: 20px;
208 right: 20px;
209 z-index: 10000;
210 padding: 12px 20px;
211 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
212 color: white;
213 border: none;
214 border-radius: 8px;
215 font-size: 14px;
216 font-weight: 600;
217 cursor: pointer;
218 box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
219 transition: all 0.3s ease;
220 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
221 `;
222
223 // Add hover effect
224 button.onmouseenter = () => {
225 button.style.transform = 'translateY(-2px)';
226 button.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6)';
227 };
228 button.onmouseleave = () => {
229 button.style.transform = 'translateY(0)';
230 button.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4)';
231 };
232
233 // Add click handler
234 button.onclick = downloadProjectAsZip;
235
236 // Add button to page
237 document.body.appendChild(button);
238 console.log('Rork Project Downloader: Download button added successfully');
239 }
240
241 // Wait for page to load and add button
242 function init() {
243 if (document.body) {
244 addDownloadButton();
245 } else {
246 // If body doesn't exist yet, wait for it
247 const observer = new MutationObserver((mutations, obs) => {
248 if (document.body) {
249 addDownloadButton();
250 obs.disconnect();
251 }
252 });
253 observer.observe(document.documentElement, { childList: true, subtree: true });
254 }
255 }
256
257 // Initialize when DOM is ready
258 if (document.readyState === 'loading') {
259 document.addEventListener('DOMContentLoaded', init);
260 } else {
261 init();
262 }
263
264 // Also try to add button after a delay to ensure page is fully loaded
265 setTimeout(init, 2000);
266 setTimeout(init, 5000);
267
268 console.log('Rork Project Downloader: Initialization complete');
269})();