Spaces:
Runtime error
Runtime error
Update index.js
Browse files
index.js
CHANGED
|
@@ -6,10 +6,15 @@ const fs = require('fs');
|
|
| 6 |
const os = require('os');
|
| 7 |
const { execSync, spawnSync } = require('child_process');
|
| 8 |
const axios = require('axios');
|
|
|
|
| 9 |
|
| 10 |
const app = express();
|
| 11 |
const PORT = 3000;
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
app.use(express.json({ limit: '200mb' }));
|
| 14 |
app.use(express.static(path.join(__dirname, 'public')));
|
| 15 |
|
|
@@ -62,6 +67,22 @@ function checkTools() {
|
|
| 62 |
|
| 63 |
// ββ ROUTES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
// GET /tools β let frontend know what's available
|
| 66 |
app.get('/tools', (req, res) => {
|
| 67 |
res.json(checkTools());
|
|
@@ -69,7 +90,9 @@ app.get('/tools', (req, res) => {
|
|
| 69 |
|
| 70 |
// GET /session/:token β check if a session is still alive
|
| 71 |
app.get('/session/:token', (req, res) => {
|
| 72 |
-
const sessionDir =
|
|
|
|
|
|
|
| 73 |
const alive = fs.existsSync(path.join(sessionDir, 'original.apk'));
|
| 74 |
res.json({ alive });
|
| 75 |
});
|
|
@@ -91,13 +114,25 @@ app.get('/debug', (req, res) => {
|
|
| 91 |
});
|
| 92 |
|
| 93 |
// GET /init β load bundled APK from disk, no upload needed
|
| 94 |
-
const BUNDLED_APK
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
app.get('/init', (req, res) => {
|
| 97 |
if (!fs.existsSync(BUNDLED_APK)) {
|
| 98 |
return res.status(404).json({ error: 'Bundled APK not found on server' });
|
| 99 |
}
|
| 100 |
try {
|
|
|
|
| 101 |
const zip = new AdmZip(BUNDLED_APK);
|
| 102 |
const entry = zip.getEntries().find(e =>
|
| 103 |
e.entryName.replace(/\\/g, '/').toLowerCase() === 'assets/characters.txt'
|
|
@@ -106,13 +141,9 @@ app.get('/init', (req, res) => {
|
|
| 106 |
const names = zip.getEntries().map(e => e.entryName).join(', ');
|
| 107 |
return res.status(400).json({ error: `assets/characters.txt not found. Entries: ${names}` });
|
| 108 |
}
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
const token
|
| 112 |
-
const sessionDir = path.join(os.tmpdir(), token);
|
| 113 |
-
fs.mkdirSync(sessionDir);
|
| 114 |
-
fs.copyFileSync(BUNDLED_APK, path.join(sessionDir, 'original.apk'));
|
| 115 |
-
fs.writeFileSync(path.join(sessionDir, 'original_name.txt'), path.basename(BUNDLED_APK));
|
| 116 |
res.json({ token, characters: content });
|
| 117 |
} catch (err) {
|
| 118 |
res.status(500).json({ error: err.message });
|
|
@@ -248,20 +279,11 @@ app.post('/patch', express.json({ limit: '10mb' }), (req, res) => {
|
|
| 248 |
throw new Error('No signing tool found. Install JDK (keytool+jarsigner) or Android SDK build-tools (apksigner+zipalign) and make sure they are on PATH.');
|
| 249 |
}
|
| 250 |
|
| 251 |
-
// 5.
|
| 252 |
-
const
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
res.setHeader('Content-Disposition', `attachment; filename="${outName}"`);
|
| 258 |
-
res.setHeader('Content-Type', 'application/vnd.android.package-archive');
|
| 259 |
-
const stream = fs.createReadStream(signedApk);
|
| 260 |
-
stream.pipe(res);
|
| 261 |
-
stream.on('end', () => {
|
| 262 |
-
// Cleanup session after download
|
| 263 |
-
try { fs.rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
| 264 |
-
});
|
| 265 |
|
| 266 |
} catch (err) {
|
| 267 |
console.error(err);
|
|
@@ -277,6 +299,6 @@ app.get('/', (req, res) => {
|
|
| 277 |
// ββ START βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 278 |
app.listen(7860, () => {
|
| 279 |
console.log(`\n𦸠Super City Editor backend running at http://localhost:${PORT}`);
|
| 280 |
-
console.log(
|
| 281 |
try { ensureKeystore(); } catch (e) { console.warn('Keystore not ready yet:', e.message); }
|
| 282 |
});
|
|
|
|
| 6 |
const os = require('os');
|
| 7 |
const { execSync, spawnSync } = require('child_process');
|
| 8 |
const axios = require('axios');
|
| 9 |
+
const crypto = require('crypto');
|
| 10 |
|
| 11 |
const app = express();
|
| 12 |
const PORT = 3000;
|
| 13 |
|
| 14 |
+
// ββ DOWNLOAD HASH βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
// A stable hash generated once at startup. Share this URL to download the APK.
|
| 16 |
+
const DOWNLOAD_HASH = crypto.randomBytes(16).toString('hex');
|
| 17 |
+
|
| 18 |
app.use(express.json({ limit: '200mb' }));
|
| 19 |
app.use(express.static(path.join(__dirname, 'public')));
|
| 20 |
|
|
|
|
| 67 |
|
| 68 |
// ββ ROUTES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
|
| 70 |
+
// GET /download-link β returns the current download URL
|
| 71 |
+
app.get('/download-link', (req, res) => {
|
| 72 |
+
const patched = fs.existsSync(path.join(__dirname, 'supercity_patched.apk'));
|
| 73 |
+
res.json({ url: `/download/${DOWNLOAD_HASH}`, patched });
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
// GET /download/:hash β serve the patched APK only
|
| 77 |
+
app.get('/download/:hash', (req, res) => {
|
| 78 |
+
if (req.params.hash !== DOWNLOAD_HASH) return res.status(403).json({ error: 'Invalid download link' });
|
| 79 |
+
const patched = path.join(__dirname, 'supercity_patched.apk');
|
| 80 |
+
if (!fs.existsSync(patched)) return res.status(404).json({ error: 'No patched APK yet β edit and export first' });
|
| 81 |
+
res.setHeader('Content-Disposition', 'attachment; filename="supercity_modded.apk"');
|
| 82 |
+
res.setHeader('Content-Type', 'application/vnd.android.package-archive');
|
| 83 |
+
fs.createReadStream(patched).pipe(res);
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
// GET /tools β let frontend know what's available
|
| 87 |
app.get('/tools', (req, res) => {
|
| 88 |
res.json(checkTools());
|
|
|
|
| 90 |
|
| 91 |
// GET /session/:token β check if a session is still alive
|
| 92 |
app.get('/session/:token', (req, res) => {
|
| 93 |
+
const sessionDir = req.params.token === 'persistent'
|
| 94 |
+
? PERSISTENT_DIR
|
| 95 |
+
: path.join(os.tmpdir(), req.params.token);
|
| 96 |
const alive = fs.existsSync(path.join(sessionDir, 'original.apk'));
|
| 97 |
res.json({ alive });
|
| 98 |
});
|
|
|
|
| 114 |
});
|
| 115 |
|
| 116 |
// GET /init β load bundled APK from disk, no upload needed
|
| 117 |
+
const BUNDLED_APK = path.join(__dirname, 'super-city-2.010.64-mod-t-5play.apk');
|
| 118 |
+
const PERSISTENT_DIR = path.join(os.tmpdir(), 'supercity-persistent');
|
| 119 |
+
|
| 120 |
+
// Ensure persistent session dir exists with the APK linked/copied once
|
| 121 |
+
function ensurePersistentSession() {
|
| 122 |
+
if (!fs.existsSync(PERSISTENT_DIR)) fs.mkdirSync(PERSISTENT_DIR, { recursive: true });
|
| 123 |
+
const dest = path.join(PERSISTENT_DIR, 'original.apk');
|
| 124 |
+
if (!fs.existsSync(dest)) {
|
| 125 |
+
fs.copyFileSync(BUNDLED_APK, dest);
|
| 126 |
+
fs.writeFileSync(path.join(PERSISTENT_DIR, 'original_name.txt'), path.basename(BUNDLED_APK));
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
|
| 130 |
app.get('/init', (req, res) => {
|
| 131 |
if (!fs.existsSync(BUNDLED_APK)) {
|
| 132 |
return res.status(404).json({ error: 'Bundled APK not found on server' });
|
| 133 |
}
|
| 134 |
try {
|
| 135 |
+
ensurePersistentSession();
|
| 136 |
const zip = new AdmZip(BUNDLED_APK);
|
| 137 |
const entry = zip.getEntries().find(e =>
|
| 138 |
e.entryName.replace(/\\/g, '/').toLowerCase() === 'assets/characters.txt'
|
|
|
|
| 141 |
const names = zip.getEntries().map(e => e.entryName).join(', ');
|
| 142 |
return res.status(400).json({ error: `assets/characters.txt not found. Entries: ${names}` });
|
| 143 |
}
|
| 144 |
+
const content = entry.getData().toString('utf8');
|
| 145 |
+
// Use a stable token derived from the APK filename β same across page reloads
|
| 146 |
+
const token = 'persistent';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
res.json({ token, characters: content });
|
| 148 |
} catch (err) {
|
| 149 |
res.status(500).json({ error: err.message });
|
|
|
|
| 279 |
throw new Error('No signing tool found. Install JDK (keytool+jarsigner) or Android SDK build-tools (apksigner+zipalign) and make sure they are on PATH.');
|
| 280 |
}
|
| 281 |
|
| 282 |
+
// 5. Save to fixed output path and return download URL
|
| 283 |
+
const PATCHED_OUTPUT = path.join(__dirname, 'supercity_patched.apk');
|
| 284 |
+
fs.copyFileSync(signedApk, PATCHED_OUTPUT);
|
| 285 |
+
try { fs.rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
| 286 |
+
res.json({ url: `/download/${DOWNLOAD_HASH}` });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
} catch (err) {
|
| 289 |
console.error(err);
|
|
|
|
| 299 |
// ββ START βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 300 |
app.listen(7860, () => {
|
| 301 |
console.log(`\n𦸠Super City Editor backend running at http://localhost:${PORT}`);
|
| 302 |
+
console.log(`\n⬠Direct APK download: http://localhost:${PORT}/download/${DOWNLOAD_HASH}\n`);
|
| 303 |
try { ensureKeystore(); } catch (e) { console.warn('Keystore not ready yet:', e.message); }
|
| 304 |
});
|