almeira-web/scripts/import_wp_full_to_appwrite.js

227 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs');
const path = require('path');
const { Client, Databases, Storage, ID, Query } = require('node-appwrite');
const { InputFile } = require('node-appwrite/file');
require('dotenv').config({ path: '.env.local' });
const { applyTablesDbShim } = require('../src/lib/appwrite-tables-shim');
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://nyc.cloud.appwrite.io/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '6aef63d9002c07371b40';
const API_KEY = process.env.APPWRITE_API_KEY;
const DB_ID = process.env.APPWRITE_DATABASE_ID || 'almeira';
const BUCKET_ID = process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID || 'media_bucket';
const NOTICIA_TYPE_ID = '69c2b91400362d5d52b6';
const WP_DATA_PATH = path.join(__dirname, 'wp_extracted_data.json');
const IMG_DIR = 'C:/tmp/wp_images';
const client = new Client().setEndpoint(ENDPOINT).setProject(PROJECT_ID).setKey(API_KEY);
applyTablesDbShim(client);
const databases = new Databases(client);
const storage = new Storage(client);
function safeFileKey(relPath) {
return relPath.replace(/[^a-zA-Z0-9._-]/g, '_');
}
function stripHtml(html) {
if (!html) return '';
return html
.replace(/<[^>]*>?/gm, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&quot;/gi, '"')
.replace(/&#8220;|&#8221;/gi, '"')
.replace(/&#8216;|&#8217;/gi, "'")
.replace(/&#8211;|&#8212;/gi, '-')
.replace(/\s+/g, ' ')
.trim();
}
function formatWpContent(raw) {
if (!raw) return '<p></p>';
let content = raw.trim();
if (!content.includes('<p') && !content.includes('<div') && !content.includes('<h')) {
content = content
.split(/\r?\n\r?\n/)
.map((p) => p.trim())
.filter(Boolean)
.map((p) => `<p>${p.replace(/\r?\n/g, '<br/>')}</p>`)
.join('\n');
}
return content.substring(0, 95000);
}
async function runPool(items, concurrency, fn) {
let idx = 0;
const workers = Array.from({ length: concurrency }, async () => {
while (true) {
const current = idx++;
if (current >= items.length) break;
await fn(items[current], current);
}
});
await Promise.all(workers);
}
async function run() {
const wpData = JSON.parse(fs.readFileSync(WP_DATA_PATH, 'utf8'));
console.log('===================================================================');
console.log('🚀 IMPORTACIÓN HISTÓRICA DE WORDPRESS A APPWRITE CLOUD');
console.log(`Categorías: ${wpData.categories.length} | Posts históricos: ${wpData.posts.length}`);
console.log('===================================================================\n');
// 1. Cargar categorías existentes
console.log('1️⃣ Sincronizando categorías de WordPress...');
const existingCatSlugs = new Map();
let cursor = null;
while (true) {
const q = [Query.limit(100)];
if (cursor) q.push(Query.cursorAfter(cursor));
const res = await databases.listDocuments(DB_ID, 'categories', q);
for (const c of res.documents) {
existingCatSlugs.set(c.slug, c.$id);
}
if (res.documents.length < 100) break;
cursor = res.documents[res.documents.length - 1].$id;
}
let catsCreated = 0;
await runPool(wpData.categories, 8, async (cat) => {
const slug = (cat.slug || '').substring(0, 95);
const nombre = stripHtml(cat.nombre || slug).substring(0, 95);
if (!slug || existingCatSlugs.has(slug)) return;
try {
const doc = await databases.createDocument(DB_ID, 'categories', ID.unique(), {
nombre,
slug,
});
existingCatSlugs.set(slug, doc.$id);
catsCreated++;
} catch (_) {}
});
console.log(`✓ Categorías listas (nuevas creadas: ${catsCreated}, total: ${existingCatSlugs.size})\n`);
// 2. Cargar slugs de posts existentes para evitar duplicados si se re-ejecuta
console.log('2️⃣ Verificando posts existentes en Appwrite Cloud...');
const existingPostSlugs = new Set();
cursor = null;
while (true) {
const q = [Query.limit(100), Query.select(['$id', 'slug'])];
if (cursor) q.push(Query.cursorAfter(cursor));
const res = await databases.listDocuments(DB_ID, 'posts', q);
for (const p of res.documents) {
if (p.slug) existingPostSlugs.add(p.slug);
}
if (res.documents.length < 100) break;
cursor = res.documents[res.documents.length - 1].$id;
}
console.log(`✓ Slugs ya existentes en Appwrite: ${existingPostSlugs.size}\n`);
// 3. Ordenar posts del más reciente al más antiguo (2026 -> 2020)
const sortedPosts = [...wpData.posts].sort((a, b) => new Date(b.date) - new Date(a.date));
const toImport = sortedPosts.filter((p) => p.slug && !existingPostSlugs.has(p.slug.substring(0, 250)));
console.log(`3️⃣ Importando ${toImport.length} publicaciones históricas con sus imágenes y links originales...\n`);
const mediaCache = new Map(); // imageRelPath -> Promise<mediaDocId>
let importedPosts = 0;
let uploadedImages = 0;
let failedPosts = 0;
async function getOrUploadMedia(post) {
if (!post.imageRelPath) return null;
const cleanRel = post.imageRelPath.replace(/^\/+/, '');
if (mediaCache.has(cleanRel)) {
return mediaCache.get(cleanRel);
}
const promise = (async () => {
const localImgPath = path.join(IMG_DIR, safeFileKey(cleanRel) + '.jpg');
if (!fs.existsSync(localImgPath)) return null;
try {
const file = await storage.createFile(
BUCKET_ID,
ID.unique(),
InputFile.fromPath(localImgPath, path.posix.basename(cleanRel).replace(/[^a-zA-Z0-9._-]/g, '_') || 'wp_image.jpg')
);
const tituloMedia = stripHtml(post.imageTitle || post.title || 'Imagen WordPress').substring(0, 250) || 'Imagen';
const altMedia = stripHtml(post.imageAlt || post.title || '').substring(0, 490);
const mediaDoc = await databases.createDocument(DB_ID, 'media', ID.unique(), {
file_id: file.$id,
bucket_id: BUCKET_ID,
titulo: tituloMedia,
alt_text: altMedia,
tipo: 'imagen',
});
uploadedImages++;
return mediaDoc.$id;
} catch (err) {
return null;
}
})();
mediaCache.set(cleanRel, promise);
return promise;
}
await runPool(toImport, 6, async (post, i) => {
try {
const mediaDocId = await getOrUploadMedia(post);
const cleanTitle = stripHtml(post.title || 'Sin título').substring(0, 250) || 'Sin título';
const plainExcerpt = stripHtml(post.excerpt || post.content || cleanTitle);
const resumen = plainExcerpt.substring(0, 480) || cleanTitle;
const seoDesc = plainExcerpt.substring(0, 280) || cleanTitle;
const slug = post.slug.substring(0, 250);
let isoDate;
try {
isoDate = new Date(post.date.replace(' ', 'T') + 'Z').toISOString();
} catch (_) {
isoDate = new Date().toISOString();
}
const data = {
titulo: cleanTitle,
slug,
tipo: 'noticia',
tipo_publicacion_id: NOTICIA_TYPE_ID,
estado: 'publicado',
fecha_publicacion: isoDate,
contenido: formatWpContent(post.content),
resumen_redes: resumen,
seo_titulo: cleanTitle.substring(0, 115),
seo_descripcion: seoDesc,
...(mediaDocId ? { imagen_destacada_id: mediaDocId } : {}),
};
await databases.createDocument(DB_ID, 'posts', ID.unique(), data);
importedPosts++;
if (importedPosts % 50 === 0 || importedPosts === toImport.length) {
console.log(
`[Progreso WP] ${importedPosts}/${toImport.length} posts publicados | Imágenes subidas: ${uploadedImages} | Última fecha: ${post.date.slice(0, 10)} (${slug.slice(0, 45)})`
);
}
} catch (err) {
failedPosts++;
if (failedPosts <= 20) {
console.error(`! Error en post WP ${post.id} (${post.slug}):`, err.message);
}
}
});
console.log('\n===================================================================');
console.log(`🎉 ¡MIGRACIÓN HISTÓRICA DE WORDPRESS COMPLETADA!`);
console.log(` - Publicaciones importadas: ${importedPosts}`);
console.log(` - Imágenes destacadas subidas: ${uploadedImages}`);
console.log(` - Fallos: ${failedPosts}`);
console.log('===================================================================');
}
run().catch((e) => {
console.error('Fatal error:', e);
process.exit(1);
});