Rork Project Downloader

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})();