diff --git a/GEMINI.md b/GEMINI.md index 305fa9d..d0bc3d9 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -5,6 +5,7 @@ **InstaArchive** is a high-performance, React-based Progressive Web App (PWA) designed to browse and explore archived Instagram data with a native-feeling interface. It supports both local directory loading and self-hosted server modes. ### Key Technical Features +- **Permalinks:** Full synchronization between application state and URL query parameters (`?a=`, `?t=`, `?p=`). Supports deep-linking to archives, tabs, and specific posts. - **Persistent Caching:** Uses `idb-keyval` (IndexedDB) to cache parsed metadata and server-side media URLs. Subsequent loads of the same archive are instant. - **Glassy Scanner UI:** A custom-built glassmorphism scanning dashboard with throttled (1s) dynamic blurred backgrounds and a high-density system log. - **Support for Multiple Formats:** Recognizes official Instagram export structures and Instaloader regex-based naming conventions. @@ -31,6 +32,13 @@ - `npm run server`: Start the backend server to scan `./_sample-archives`. - `npm run lint`: Execute TypeScript type-checking. +## Troubleshooting Cache (PWA) +Since the app is a PWA, the browser may cache old JavaScript bundles. If new features don't appear: +1. Open DevTools -> Application -> Service Workers. +2. Click **Unregister** for the localhost service worker. +3. Go to **Storage** and click **Clear site data**. +4. Perform a Hard Refresh (`Ctrl + Shift + R`). + ## Production Deployment The project is containerized and available on GHCR. It expects a volume mount at `/archives` containing subdirectories for each user. diff --git a/README.md b/README.md index 32c0c0d..f1ae551 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ A high-performance React PWA for browsing archived Instagram data with a native- ## Features - **Persistent Caching**: Uses IndexedDB to store parsed archives locally. Subsequent loads are near-instant. +- **Permalinks**: State is synchronized with the URL, allowing you to share direct links to archives, tabs, or specific posts. - **Glassy Scanning UI**: A modern, translucent white terminal experience with a dynamic blurred background of your media. - **Local Privacy**: All processing is done client-side. Even when using the self-hosted version, your media is processed locally in your browser. - **Multiple Formats**: Supports official Instagram JSON exports and Instaloader regex-based naming conventions. diff --git a/dist-server/server.js b/dist-server/server.js new file mode 100644 index 0000000..80c3c16 --- /dev/null +++ b/dist-server/server.js @@ -0,0 +1,137 @@ +import express from 'express'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import dotenv from 'dotenv'; +import os from 'os'; +dotenv.config(); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const app = express(); +const PORT = process.env.PORT || 3001; +const ARCHIVES_DIR = path.resolve(process.env.ARCHIVES_DIR || path.join(__dirname, '_sample-archives')); +console.log(`[Server] Initializing...`); +console.log(`[Server] Running as user: ${os.userInfo().username} (UID: ${os.userInfo().uid}, GID: ${os.userInfo().gid})`); +console.log(`[Server] Environment ARCHIVES_DIR: ${process.env.ARCHIVES_DIR}`); +console.log(`[Server] Resolved ARCHIVES_DIR: ${ARCHIVES_DIR}`); +// Ensure archives directory exists +if (!fs.existsSync(ARCHIVES_DIR)) { + console.warn(`[Server] Warning: Archives directory not found at ${ARCHIVES_DIR}. Creating it...`); + try { + fs.mkdirSync(ARCHIVES_DIR, { recursive: true }); + } + catch (err) { + console.error(`[Server] Failed to create archives directory:`, err); + } +} +else { + console.log(`[Server] Archives directory exists.`); +} +app.use(express.json()); +// API: List archives (subdirectories in ARCHIVES_DIR) +app.get('/api/archives', (req, res) => { + try { + console.log(`[API] Listing archives from ${ARCHIVES_DIR}...`); + const items = fs.readdirSync(ARCHIVES_DIR, { withFileTypes: true }); + console.log(`[API] Found ${items.length} total items in archives directory.`); + const archives = items + .filter(item => { + const isDir = item.isDirectory(); + const isHidden = item.name.startsWith('.') || item.name.startsWith('@') || item.name.startsWith('_'); + if (!isDir) + return false; + if (isHidden) { + console.log(`[API] Skipping system/hidden directory: ${item.name}`); + return false; + } + return true; + }) + .map(item => { + // Try to find a profile pic or first image for the thumbnail + const archivePath = path.join(ARCHIVES_DIR, item.name); + try { + const files = fs.readdirSync(archivePath); + console.log(`[API] Found archive: ${item.name} (${files.length} files)`); + let thumbnail = ''; + const profilePic = files.find(f => f.toLowerCase().includes('_profile_pic.jpg') || f.toLowerCase() === `${item.name.toLowerCase()}.jpg`); + if (profilePic) { + thumbnail = `/archives/${item.name}/${profilePic}`; + } + else { + const firstImage = files.find(f => /\.(jpg|jpeg|png|webp)$/i.test(f)); + if (firstImage) + thumbnail = `/archives/${item.name}/${firstImage}`; + } + return { + name: item.name, + thumbnail, + path: item.name, + fileCount: files.length + }; + } + catch (e) { + console.error(`[API] Could not read subdirectory ${item.name}:`, e); + return null; + } + }) + .filter(Boolean); + console.log(`[API] Returning ${archives.length} validated archives.`); + res.json(archives); + } + catch (err) { + if (err.code === 'EACCES') { + console.error(`[API] Permission Denied! The server (UID ${os.userInfo().uid}) cannot read ${ARCHIVES_DIR}.`); + console.error(`[API] Hint: If using Linux/Docker, check folder permissions (chmod 755) or SELinux context (append :z to your volume mount).`); + } + else { + console.error('[API] Error listing archives:', err); + } + res.status(500).json({ error: 'Permission denied or failed to list archives' }); + } +}); +// API: List all files in an archive (recursive) +app.get('/api/archives/:name/files', (req, res) => { + const archiveName = req.params.name; + const archivePath = path.join(ARCHIVES_DIR, archiveName); + if (!fs.existsSync(archivePath)) { + return res.status(404).json({ error: 'Archive not found' }); + } + try { + const walk = (dir, base = '') => { + let results = []; + const list = fs.readdirSync(dir); + list.forEach(file => { + const filePath = path.join(dir, file); + const relativePath = path.join(base, file); + const stat = fs.statSync(filePath); + if (stat && stat.isDirectory()) { + results = results.concat(walk(filePath, relativePath)); + } + else { + results.push(relativePath); + } + }); + return results; + }; + const files = walk(archivePath); + res.json(files); + } + catch (err) { + console.error('Error listing files:', err); + res.status(500).json({ error: 'Failed to list files' }); + } +}); +// Serve archive files +app.use('/archives', express.static(ARCHIVES_DIR)); +// Serve production frontend +const distPath = path.join(__dirname, 'dist'); +if (fs.existsSync(distPath)) { + app.use(express.static(distPath)); + app.get('*', (req, res) => { + res.sendFile(path.join(distPath, 'index.html')); + }); +} +app.listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}`); + console.log(`Serving archives from: ${ARCHIVES_DIR}`); +}); diff --git a/package.json b/package.json index f2ad7c1..1936838 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-example", "private": true, - "version": "1.1.2", + "version": "1.1.3", "type": "module", "scripts": { "dev": "vite --port=3000 --host=0.0.0.0",