205 lines
6.6 KiB
JavaScript
205 lines
6.6 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const zlib = require('zlib');
|
|
const sharp = require('sharp');
|
|
|
|
const TAR_GZ_PATH = 'C:/Users/Carlos/Downloads/u858407019.almeiraproducciones-com.20260607074508.tar.gz';
|
|
const WP_DATA_PATH = path.join(__dirname, 'wp_extracted_data.json');
|
|
const OUT_DIR = 'C:/tmp/wp_images';
|
|
|
|
if (!fs.existsSync(OUT_DIR)) {
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
}
|
|
|
|
function safeFileKey(relPath) {
|
|
return relPath.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
}
|
|
|
|
async function run() {
|
|
const wpData = JSON.parse(fs.readFileSync(WP_DATA_PATH, 'utf8'));
|
|
const neededRelPaths = new Map(); // normalized lower relPath -> original relPath
|
|
const neededBasenames = new Map(); // lower basename -> original relPath
|
|
|
|
for (const post of wpData.posts) {
|
|
if (post.imageRelPath) {
|
|
const clean = post.imageRelPath.replace(/^\/+/, '');
|
|
neededRelPaths.set(clean.toLowerCase(), clean);
|
|
neededBasenames.set(path.posix.basename(clean).toLowerCase(), clean);
|
|
}
|
|
}
|
|
|
|
console.log(`Total imágenes destacadas únicas requeridas por los ${wpData.posts.length} posts: ${neededRelPaths.size}`);
|
|
|
|
let extractedCount = 0;
|
|
let scannedFiles = 0;
|
|
let nextLongName = null;
|
|
let nextPaxPath = null;
|
|
|
|
// Streaming tar reader using a growable/ring buffer list
|
|
let chunks = [];
|
|
let totalBuffered = 0;
|
|
|
|
function consumeBytes(n) {
|
|
const out = Buffer.allocUnsafe(n);
|
|
let offset = 0;
|
|
while (offset < n) {
|
|
const first = chunks[0];
|
|
const toCopy = Math.min(first.length, n - offset);
|
|
first.copy(out, offset, 0, toCopy);
|
|
offset += toCopy;
|
|
if (toCopy === first.length) {
|
|
chunks.shift();
|
|
} else {
|
|
chunks[0] = first.subarray(toCopy);
|
|
}
|
|
}
|
|
totalBuffered -= n;
|
|
return out;
|
|
}
|
|
|
|
function skipBytes(n) {
|
|
let remaining = n;
|
|
while (remaining > 0) {
|
|
const first = chunks[0];
|
|
if (first.length <= remaining) {
|
|
remaining -= first.length;
|
|
chunks.shift();
|
|
} else {
|
|
chunks[0] = first.subarray(remaining);
|
|
remaining = 0;
|
|
}
|
|
}
|
|
totalBuffered -= n;
|
|
}
|
|
|
|
let state = 'HEADER'; // 'HEADER' | 'DATA' | 'SKIP'
|
|
let currentEntrySize = 0;
|
|
let currentEntryPadded = 0;
|
|
let currentEntryType = '0';
|
|
let currentMatchedRel = null;
|
|
|
|
const pendingSharpPromises = [];
|
|
|
|
const gunzip = zlib.createGunzip({ chunkSize: 1024 * 1024 });
|
|
const readStream = fs.createReadStream(TAR_GZ_PATH, { highWaterMark: 1024 * 1024 });
|
|
const stream = readStream.pipe(gunzip);
|
|
|
|
for await (const chunk of stream) {
|
|
chunks.push(chunk);
|
|
totalBuffered += chunk.length;
|
|
|
|
while (true) {
|
|
if (state === 'HEADER') {
|
|
if (totalBuffered < 512) break;
|
|
const header = consumeBytes(512);
|
|
|
|
// Check empty block (end of archive)
|
|
let allZero = true;
|
|
for (let i = 0; i < 512; i++) {
|
|
if (header[i] !== 0) {
|
|
allZero = false;
|
|
break;
|
|
}
|
|
}
|
|
if (allZero) continue;
|
|
|
|
const nameRaw = header.toString('utf8', 0, 100).replace(/\0.*$/, '');
|
|
const sizeOctal = header.toString('ascii', 124, 136).replace(/\0.*$/, '').trim();
|
|
const typeFlag = String.fromCharCode(header[156] || 48);
|
|
const prefixRaw = header.toString('utf8', 345, 500).replace(/\0.*$/, '');
|
|
|
|
let fullPath = prefixRaw ? `${prefixRaw}/${nameRaw}` : nameRaw;
|
|
if (nextLongName) {
|
|
fullPath = nextLongName;
|
|
nextLongName = null;
|
|
}
|
|
if (nextPaxPath) {
|
|
fullPath = nextPaxPath;
|
|
nextPaxPath = null;
|
|
}
|
|
|
|
currentEntrySize = parseInt(sizeOctal, 8) || 0;
|
|
currentEntryPadded = Math.ceil(currentEntrySize / 512) * 512;
|
|
currentEntryType = typeFlag;
|
|
|
|
if (typeFlag === 'L' || typeFlag === 'x') {
|
|
state = 'DATA';
|
|
continue;
|
|
}
|
|
|
|
if (typeFlag === '0' || typeFlag === '\0') {
|
|
scannedFiles++;
|
|
if (scannedFiles % 10000 === 0) {
|
|
console.log(`... escaneados ${scannedFiles} archivos en tar.gz | extraídas: ${extractedCount}/${neededRelPaths.size}`);
|
|
}
|
|
|
|
const uploadsIdx = fullPath.indexOf('wp-content/uploads/');
|
|
if (uploadsIdx !== -1 && currentEntrySize > 0) {
|
|
const relInUploads = fullPath.slice(uploadsIdx + 'wp-content/uploads/'.length);
|
|
const lowerRel = relInUploads.toLowerCase();
|
|
const lowerBase = path.posix.basename(relInUploads).toLowerCase();
|
|
|
|
let matched = neededRelPaths.get(lowerRel) || neededBasenames.get(lowerBase) || null;
|
|
if (matched) {
|
|
const outFile = path.join(OUT_DIR, safeFileKey(matched) + '.jpg');
|
|
if (!fs.existsSync(outFile)) {
|
|
currentMatchedRel = matched;
|
|
state = 'DATA';
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
state = 'SKIP';
|
|
} else if (state === 'SKIP') {
|
|
if (totalBuffered < currentEntryPadded) break;
|
|
if (currentEntryPadded > 0) skipBytes(currentEntryPadded);
|
|
state = 'HEADER';
|
|
} else if (state === 'DATA') {
|
|
if (totalBuffered < currentEntryPadded) break;
|
|
const dataBlock = consumeBytes(currentEntryPadded);
|
|
const fileData = dataBlock.subarray(0, currentEntrySize);
|
|
|
|
if (currentEntryType === 'L') {
|
|
nextLongName = fileData.toString('utf8').replace(/\0.*$/, '');
|
|
} else if (currentEntryType === 'x') {
|
|
const paxStr = fileData.toString('utf8');
|
|
const m = paxStr.match(/\d+ path=([^\n]+)\n/);
|
|
if (m) nextPaxPath = m[1];
|
|
} else if (currentMatchedRel) {
|
|
const targetRel = currentMatchedRel;
|
|
currentMatchedRel = null;
|
|
const outFile = path.join(OUT_DIR, safeFileKey(targetRel) + '.jpg');
|
|
extractedCount++;
|
|
|
|
const p = sharp(fileData)
|
|
.resize(800, 500, { fit: 'inside', withoutEnlargement: true })
|
|
.jpeg({ quality: 80 })
|
|
.toFile(outFile)
|
|
.catch(() => {
|
|
// Fallback: write raw buffer if sharp fails on an unusual image
|
|
try { fs.writeFileSync(outFile, fileData); } catch (_) {}
|
|
});
|
|
pendingSharpPromises.push(p);
|
|
|
|
if (pendingSharpPromises.length >= 50) {
|
|
await Promise.all(pendingSharpPromises.splice(0, pendingSharpPromises.length));
|
|
}
|
|
}
|
|
|
|
state = 'HEADER';
|
|
}
|
|
}
|
|
}
|
|
|
|
await Promise.all(pendingSharpPromises);
|
|
console.log(`\n✅ Extracción de imágenes de WordPress finalizada: ${extractedCount} imágenes listas en ${OUT_DIR}`);
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error('Error:', e);
|
|
process.exit(1);
|
|
});
|
|
|