Mapeador de carpetas de codigo drive

// MapeadorDeProyectos.gs
const DOC = DocumentApp.getActiveDocument();
const COLORS = {
ROOT: '#202124',
FOLDER: '#5f6368',
FILE: '#1a73e8',
ERROR: '#ea4335',
SUCCESS: '#0f9d58',
WARNING: '#fbbc04'
};

const CONFIG_MP = {
MAX_FILE_SIZE: 2000000,
MAX_LINES: 2000,
ENCODING: "UTF-8",
FONT_FAMILY: 'Consolas',
FONT_SIZE: 9,
INDENT_SIZE: 30,
SECCION_LIMITE: 500,
SEPARADOR: '─'.repeat(50),
MAX_PROFUNDIDAD: 10,
BATCH_MIN: 5,
BATCH_MAX: 50
};

const MIME_TYPES_PERMITIDOS = [
"text/plain", "text/html", "text/css", "text/javascript", "application/javascript",
"application/json", "application/xml", "text/x-python", "text/x-java", "text/x-php",
"application/x-httpd-php", "application/x-php", "text/x-c", "text/x-c++",
"application/vnd.google-apps.script", "text/x-ruby", "text/x-perl", "text/x-shellscript",
"text/x-swift", "text/x-kotlin", "text/x-scala", "text/x-go", "text/x-rust", "text/x-typescript",
"text/x-lua", "text/x-clojure", "text/x-haskell", "text/x-arduino", "text/x-matlab",
"text/x-markdown", "text/x-yaml", "text/x-properties", "text/x-ini", "text/x-csv", "text/x-tsv"
];

/********** FUNCIONES PRINCIPALES **********/
function onOpen() {
DocumentApp.getUi().createMenu('📁 Mapeador de Proyectos')
.addItem('🗺️ Iniciar mapeo', 'iniciarMapeo')
.addToUi();
}

function iniciarMapeo() {
try {
limpiarEstado();
const folder = seleccionarCarpeta();
if (!folder) return;
const body = DOC.getBody();
body.clear();
configurarEncabezado(body, folder);
generarEstructura(folder, body);
body.appendPageBreak();
const fileList = listarArchivos(folder);
const state = inicializarEstado(folder, fileList);
guardarEstado(state);
procesarLote();
} catch (e) {
manejarError(e);
}
}

/********** FUNCIONES DE SOPORTE **********/
function seleccionarCarpeta() {
const ui = DocumentApp.getUi();
const response = ui.prompt(
'Seleccionar carpeta raíz',
'Ingrese la URL de la carpeta en Google Drive:',
ui.ButtonSet.OK_CANCEL
);
if (response.getSelectedButton() !== ui.Button.OK) return null;
return obtenerCarpetaDesdeUrl(response.getResponseText());
}

function obtenerCarpetaDesdeUrl(url) {
const match = url.match(/[-\w]{25,}/);
if (!match) throw new Error("URL de carpeta inválida");
try {
return DriveApp.getFolderById(match[0]);
} catch (e) {
throw new Error("No se pudo acceder a la carpeta: " + e.message);
}
}

function listarArchivos(folder) {
const files = [];
const stack = [{folder: folder, path: folder.getName(), depth: 0}];
while (stack.length > 0) {
const current = stack.pop();
procesarArchivos(current, files);
if (current.depth < CONFIG_MP.MAX_PROFUNDIDAD) {
procesarSubcarpetas(current, stack);
} else {
files.push({
name: "[LÍMITE DE PROFUNDIDAD]",
path: `${current.path}/...`,
tipo: "limit",
size: 0
});
}
}
return files;
}

function procesarArchivos(current, files) {
const fileIter = current.folder.getFiles();
while (fileIter.hasNext()) {
const file = fileIter.next();
files.push({
id: file.getId(),
name: file.getName(),
path: `${current.path}/${file.getName()}`,
mimeType: file.getMimeType(),
size: file.getSize(),
tipo: "file"
});
}
}

function procesarSubcarpetas(current, stack) {
const folderIter = current.folder.getFolders();
while (folderIter.hasNext()) {
const subfolder = folderIter.next();
stack.push({
folder: subfolder,
path: `${current.path}/${subfolder.getName()}`,
depth: current.depth + 1,
tipo: "folder"
});
}
}

function generarEstructura(folder, body, nivel = 0) {
const prefijo = '│ '.repeat(nivel) + (nivel > 0 ? '├── ' : '');
procesarSubcarpetasEstructura(folder, body, nivel, prefijo);
procesarArchivosEstructura(folder, body, nivel, prefijo);
}

function procesarSubcarpetasEstructura(folder, body, nivel, prefijo) {
const subfolders = folder.getFolders();
while (subfolders.hasNext()) {
const subfolder = subfolders.next();
body.appendParagraph(prefijo + '📁 ' + subfolder.getName())
.setForegroundColor(COLORS.FOLDER);
generarEstructura(subfolder, body, nivel + 1);
}
}

function procesarArchivosEstructura(folder, body, nivel, prefijo) {
const files = folder.getFiles();
while (files.hasNext()) {
const file = files.next();
const icono = MIME_TYPES_PERMITIDOS.includes(file.getMimeType()) ? '📝' : '📦';
body.appendParagraph(prefijo + icono + ' ' + file.getName())
.setForegroundColor(COLORS.FILE);
}
}

/********** PROCESAMIENTO DE ARCHIVOS **********/
function procesarLote() {
const state = obtenerEstado();
if (!state || state.step !== "mapeo") return;
const startTime = Date.now();
const body = DOC.getBody();
const files = JSON.parse(state.fileList);
const end = Math.min(state.processedFiles + state.batchSize, state.totalFiles);
for (let i = state.processedFiles; i < end; i++) {
procesarArchivoIndividual(files[i], body, state);
}
ajustarBatchSize(state, startTime);
guardarEstado(state);
if (state.processedFiles >= state.totalFiles) {
finalizarProceso();
} else {
programarProximoLote();
}
}

function procesarArchivoIndividual(fileInfo, body, state) {
try {
if (fileInfo.tipo === "limit") {
registrarAdvertenciaProfundidad(fileInfo, body, state);
state.processedFiles++;
return;
}
const file = DriveApp.getFileById(fileInfo.id);
if (archivoValido(fileInfo)) {
agregarContenidoArchivo(file, fileInfo.path, body);
state.exitosos++;
} else {
registrarArchivoOmitido(fileInfo, body, state);
}
actualizarProgreso(++state.processedFiles, state.totalFiles);
} catch (e) {
registrarErrorArchivo(fileInfo.path, e.message, state);
actualizarProgreso(++state.processedFiles, state.totalFiles);
}
}

function archivoValido(fileInfo) {
return (
MIME_TYPES_PERMITIDOS.includes(fileInfo.mimeType) &&
fileInfo.size <= CONFIG_MP.MAX_FILE_SIZE
);
}

function registrarAdvertenciaProfundidad(fileInfo, body, state) {
const texto = `⚠️ ADVERTENCIA: Se omitieron subcarpetas en ${fileInfo.path} (límite de profundidad: ${CONFIG_MP.MAX_PROFUNDIDAD} niveles)`;
body.appendParagraph(texto)
.setForegroundColor(COLORS.WARNING)
.setFontSize(9);
state.advertencias++;
}

function registrarArchivoOmitido(fileInfo, body, state) {
const razon = fileInfo.size > CONFIG_MP.MAX_FILE_SIZE ?
"TAMAÑO EXCEDIDO" : "TIPO NO SOPORTADO";
const texto = `⏭ OMITIDO: ${fileInfo.path} (${fileInfo.mimeType}) - ${razon}`;
body.appendParagraph(texto)
.setForegroundColor(COLORS.ERROR)
.setFontSize(9);

state.omitidos++;
}

function agregarContenidoArchivo(file, ruta, body) {
const blob = file.getBlob();
const contenidoOriginal = blob.getDataAsString(CONFIG_MP.ENCODING);
// Verificación de integridad (pre-procesamiento)
const hashOriginal = calcularHash(contenidoOriginal);
const contenido = contenidoOriginal
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
// Verificación de integridad (post-procesamiento)
const hashProcesado = calcularHash(contenido);
if (hashOriginal !== hashProcesado) {
throw new Error("Alteración de contenido detectada. Hash original: " +
hashOriginal + " vs procesado: " + hashProcesado);
}
const lines = contenido.split('\n');
const truncated = lines.length > CONFIG_MP.MAX_LINES;
const lineasProcesar = truncated ? lines.slice(0, CONFIG_MP.MAX_LINES) : lines;
agregarMetadatosArchivo(body, file, ruta, lineasProcesar.length, hashOriginal);
if (lineasProcesar.length === 0) {
body.appendParagraph(`⚠️ Archivo vacío: ${ruta}`)
.setForegroundColor(COLORS.WARNING)
.setFontSize(9)
.setItalic(true);
return;
}
const totalSecciones = Math.ceil(lineasProcesar.length / CONFIG_MP.SECCION_LIMITE);
if (totalSecciones > 1) generarIndiceSecciones(body, ruta, totalSecciones);
procesarSeccionesArchivo(body, ruta, lineasProcesar, totalSecciones, truncated);
}

function calcularHash(contenido) {
return Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, contenido, Utilities.Charset.UTF_8)
.map(b => b < 0 ? b + 256 : b)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}

function agregarMetadatosArchivo(body, file, ruta, numLineas, hash) {
const sizeKB = (file.getSize() / 1024).toFixed(1);
const meta = `📄 ${ruta}\n🧾 Líneas: ${numLineas} | 📦 Tamaño: ${sizeKB}KB | 🔒 Hash: ${hash.slice(0,8)}...`;
body.appendParagraph(meta)
.setBold(true)
.setForegroundColor(COLORS.FILE)
.setFontSize(9);
body.appendParagraph(CONFIG_MP.SEPARADOR)
.setFontSize(8)
.setForegroundColor('#9e9e9e');
}

function generarIndiceSecciones(body, ruta, totalSecciones) {
body.appendParagraph(`📑 ÍNDICE DE SECCIONES (${totalSecciones}):`)
.setForegroundColor(COLORS.FOLDER);
for (let i = 0; i < totalSecciones; i++) {
const link = body.appendParagraph(`→ Sección ${i + 1}`);
link.setLinkUrl(`#${ruta}-sec${i + 1}`)
.setForegroundColor('#3367d6')
.setFontSize(9);
}
body.appendParagraph('');
}

function procesarSeccionesArchivo(body, ruta, lines, totalSecciones, truncated) {
for (let i = 0; i < totalSecciones; i++) {
const start = i * CONFIG_MP.SECCION_LIMITE;
const end = Math.min(start + CONFIG_MP.SECCION_LIMITE, lines.length);
const seccionLines = lines.slice(start, end);
agregarEncabezadoSeccion(body, ruta, i, totalSecciones);
agregarContenidoSeccion(body, seccionLines);
if (truncated && i === totalSecciones - 1) {
agregarMensajeTruncamiento(body, lines.length);
}
body.appendPageBreak();
}
}

function agregarEncabezadoSeccion(body, ruta, index, totalSecciones) {
const header = body.appendParagraph(`📑 ${ruta} [SECCIÓN ${index + 1}/${totalSecciones}]`);
header.setHeading(DocumentApp.ParagraphHeading.HEADING3)
.setForegroundColor(COLORS.FILE)
.setLinkUrl(`#${ruta}-sec${index + 1}`);
}

function agregarContenidoSeccion(body, lines) {
const container = body.appendParagraph("");
const text = container.editAsText();
lines.forEach(line => {
const processedLine = line.replace(/^ /g, '\u00A0').replace(/\t/g, '\u00A0\u00A0\u00A0\u00A0');
text.appendText(processedLine + '\n');
});
text.setFontFamily(CONFIG_MP.FONT_FAMILY)
.setFontSize(CONFIG_MP.FONT_SIZE)
.setForegroundColor('#000000');
container.setIndentStart(CONFIG_MP.INDENT_SIZE);
}

function agregarMensajeTruncamiento(body, totalLineas) {
const msg = `// ... [CONTENIDO TRUNCADO: ${totalLineas} LÍNEAS > ${CONFIG_MP.MAX_LINES} LÍMITE] ...`;
body.appendParagraph(msg)
.setFontFamily(CONFIG_MP.FONT_FAMILY)
.setFontSize(CONFIG_MP.FONT_SIZE)
.setIndentStart(CONFIG_MP.INDENT_SIZE)
.setForegroundColor(COLORS.WARNING);
}

/********** FUNCIONES AUXILIARES **********/
function inicializarEstado(folder, fileList) {
return {
folderId: folder.getId(),
totalFiles: fileList.length,
processedFiles: 0,
step: "mapeo",
fileList: JSON.stringify(fileList),
batchSize: 15,
errores: [],
exitosos: 0,
omitidos: 0,
advertencias: 0,
startTime: new Date().getTime(),
hashVerifications: 0
};
}

// ENCABEZADO PRINCIPAL CORREGIDO
function configurarEncabezado(body, folder) {
// Encabezado principal como HEADING1
body.appendParagraph(folder.getName())
.setHeading(DocumentApp.ParagraphHeading.HEADING1)
.setForegroundColor(COLORS.ROOT);

// Metadata de generación
body.appendParagraph(`Generado: ${new Date().toLocaleString()}`)
.setItalic(true);

body.appendHorizontalRule();

// Inicio de sección de estructura
body.appendParagraph("📁 Estructura de carpetas:")
.setHeading(DocumentApp.ParagraphHeading.HEADING2)
.setForegroundColor(COLORS.ROOT);
}

function actualizarProgreso(actual, total) {
const porcentaje = Math.round((actual / total) * 100);
const progreso = `⏳ Progreso: ${porcentaje}% (${actual}/${total} archivos)`;
const body = DOC.getBody();
let parrafoProgreso = null;
for (let i = 0; i < body.getNumChildren(); i++) {
try {
const parrafo = body.getChild(i).asParagraph();
const texto = parrafo.getText();
if (texto.includes('⏳ Progreso:')) {
parrafoProgreso = parrafo;
break;
}
} catch (e) {
continue;
}
}
if (parrafoProgreso) {
parrafoProgreso.replaceText(parrafoProgreso.getText(), progreso);
} else {
body.appendParagraph(progreso)
.setFontSize(9)
.setForegroundColor('#5f6368')
.setItalic(true);
}
}

function ajustarBatchSize(state, startTime) {
const elapsed = Date.now() - startTime;
if (elapsed < 4000 && state.processedFiles < state.totalFiles) {
state.batchSize = Math.min(state.batchSize + 5, CONFIG_MP.BATCH_MAX);
} else if (elapsed > 20000) {
state.batchSize = Math.max(state.batchSize - 5, CONFIG_MP.BATCH_MIN);
}
}

function programarProximoLote() {
ScriptApp.newTrigger('procesarLote')
.timeBased()
.after(1000)
.create();
}

function registrarErrorArchivo(ruta, mensaje, state) {
state.errores.push({
archivo: ruta,
mensaje: mensaje,
timestamp: new Date().toISOString()
});
DOC.getBody().appendParagraph(`❌ ERROR EN ${ruta}: ${mensaje}`)
.setForegroundColor(COLORS.ERROR)
.setFontSize(9);
}

function guardarEstado(state) {
PropertiesService.getScriptProperties().setProperty('STATE', JSON.stringify(state));
}

function obtenerEstado() {
const state = PropertiesService.getScriptProperties().getProperty('STATE');
return state ? JSON.parse(state) : null;
}

function limpiarEstado() {
PropertiesService.getScriptProperties().deleteProperty('STATE');
}

/********** FINALIZACIÓN **********/
function finalizarProceso() {
const state = obtenerEstado();
const body = DOC.getBody();
mostrarErrores(body, state);
mostrarResumenFinal(body, state);
DocumentApp.getUi().alert("✅ Mapeo completado");
limpiarEstado();
}

function mostrarErrores(body, state) {
if (!state?.errores?.length) return;
body.appendPageBreak();
body.appendParagraph("⚠️ ERRORES DETECTADOS")
.setHeading(DocumentApp.ParagraphHeading.HEADING2)
.setForegroundColor(COLORS.ERROR);
state.errores.forEach(err => {
body.appendParagraph(`❌ ${err.archivo}`)
.setForegroundColor(COLORS.ERROR);
const detalle = body.appendParagraph(`⏱️ ${err.timestamp.split('T')[1].substring(0,8)}: ${err.mensaje}`);
detalle.setIndentStart(30).setFontSize(9);
});
}

function mostrarResumenFinal(body, state) {
if (!state) return;
const tiempoTotal = Math.round((Date.now() - state.startTime)/1000);
const tiempoTexto = tiempoTotal > 60 ?
`${Math.floor(tiempoTotal/60)}m ${tiempoTotal%60}s` : `${tiempoTotal}s`;
body.appendPageBreak();
// CORRECCIÓN: HEADING1 -> HEADING3 para el título final
body.appendParagraph("✅ MAPEO COMPLETADO")
.setHeading(DocumentApp.ParagraphHeading.HEADING3) // Nivel H3
.setForegroundColor(COLORS.SUCCESS);
const resumen = [
`🗂️ Archivos totales: ${state.totalFiles}`,
`✅ Exitosos: ${state.exitosos}`,
`⏭ Omitidos: ${state.omitidos}`,
`⚠️ Advertencias: ${state.advertencias}`,
`❌ Errores: ${state.errores.length}`,
`⏱️ Tiempo total: ${tiempoTexto}`,
`🔒 Archivos verificados: ${state.exitosos}`,
`📅 Finalizado: ${new Date().toLocaleString()}`
];
resumen.forEach(linea => {
body.appendParagraph(linea)
.setFontSize(10)
.setIndentStart(30);
});
// Garantía de integridad
body.appendParagraph("\n🔐 GARANTÍA DE INTEGRIDAD")
.setBold(true);
body.appendParagraph("• Todos los archivos fueron procesados en modo de solo lectura")
.setItalic(true);
body.appendParagraph("• Se verificó hash MD5 para cada archivo antes y después del procesamiento")
.setItalic(true);
body.appendParagraph("• 0 bytes modificados en los contenidos originales")
.setItalic(true);
body.appendParagraph(`• Hash verifications: ${state.exitosos} archivos verificados`)
.setItalic(true);
}

/********** MANEJO DE ERRORES **********/
function manejarError(error) {
DocumentApp.getUi().alert(`🚨 Error crítico: ${error.message}`);
const body = DOC.getBody();
body.appendParagraph("❌ PROCESO INTERRUMPIDO POR ERROR")
.setHeading(DocumentApp.ParagraphHeading.HEADING1)
.setForegroundColor(COLORS.ERROR);
body.appendParagraph(`Error: ${error.message}`)
.setFontSize(10);
body.appendParagraph(`Stack: ${error.stack || 'No disponible'}`)
.setFontSize(8)
.setForegroundColor('#5f6368');
limpiarEstado();
}

¿Le ha resultado útil este artículo?