Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // Copyright 2025
- // Developed for a legal assistant to index large Google Drive folders.
- // This script is designed for maximum performance by using Google's REST APIs directly.
- //
- // --- IMPORTANT SETUP ---
- // This script requires a standard Google Cloud Platform (GCP) project to function.
- // Please follow the instructions in the "Help" tab of the dashboard after running the script.
- // You must enable the "Google Drive API" and "Google Sheets API" in your GCP project.
- // --- CONFIGURATION & CONSTANTS --- //
- const INDEX_SHEET_NAME = 'File Index';
- const CONFIG_SHEET_NAME = 'Configuration';
- const HEADERS = ['Parent Folder', 'File Name', 'Date Created', 'File Type', 'Link'];
- const DEFAULT_COLORS = [
- '#e0e7ff', '#fee2e2', '#d1fae5', '#fef3c7', '#f3e8ff',
- '#dbeafe', '#ffe4e6', '#cce7e0', '#fef9c3', '#ede9fe',
- '#c7d2fe', '#fecdd3', '#bbf7d0', '#fde68a', '#ddd6fe'
- ];
- const MAX_RUNTIME_MS = 5 * 60 * 1000;
- const SCRIPT_LOCK_TIMEOUT_MS = 30 * 1000;
- const SHEET_CAPACITY_THRESHOLD = 0.95; // 95%
- const DRIVE_API_V3 = 'https://www.googleapis.com/drive/v3/files';
- const SHEETS_API_V4 = 'https://sheets.googleapis.com/v4/spreadsheets';
- // --- SPREADSHEET UI & INITIALIZATION --- //
- /**
- * Creates a custom menu in the spreadsheet when the file is opened.
- * Checks for and creates necessary sheets using the Sheets API.
- */
- function onOpen() {
- SpreadsheetApp.getUi()
- .createMenu('🗂️ File Indexer')
- .addItem('🚀 Open Dashboard', 'showIndexerModal')
- .addSeparator()
- .addItem('🔍 Start/Update Index Scan', 'startIndexingProcess')
- .addToUi();
- // Ensure sheets exist using API to minimize use of SpreadsheetApp service
- try {
- const ss = SpreadsheetApp.getActiveSpreadsheet();
- const spreadsheetId = ss.getId();
- const response = _apiRequest(`${SHEETS_API_V4}/${spreadsheetId}?fields=sheets.properties.title`);
- const sheetTitles = response.sheets.map(s => s.properties.title);
- const requests = [];
- if (!sheetTitles.includes(INDEX_SHEET_NAME)) {
- requests.push({
- addSheet: {
- properties: { title: INDEX_SHEET_NAME }
- }
- });
- }
- if (!sheetTitles.includes(CONFIG_SHEET_NAME)) {
- requests.push({
- addSheet: {
- properties: { title: CONFIG_SHEET_NAME, gridProperties: { "frozenRowCount": 0 } }
- }
- });
- }
- if (requests.length > 0) {
- _apiRequest(`${SHEETS_API_V4}/${spreadsheetId}:batchUpdate`, {
- method: 'post',
- payload: { requests: requests }
- });
- // A small delay to allow sheet creation before trying to add headers
- Utilities.sleep(2000);
- _initializeIndexSheet(spreadsheetId);
- }
- } catch (e) {
- SpreadsheetApp.getUi().alert(`Failed to initialize sheets: ${e.message}`);
- }
- }
- /**
- * Sets up headers and formatting for the new index sheet.
- */
- function _initializeIndexSheet(spreadsheetId) {
- const requests = [{
- updateCells: {
- rows: [{
- values: HEADERS.map(h => ({ userEnteredValue: { stringValue: h }, userEnteredFormat: { textFormat: { bold: true } } }))
- }],
- fields: "userEnteredValue,userEnteredFormat.textFormat.bold",
- start: { sheetId: _getSheetIdByName(spreadsheetId, INDEX_SHEET_NAME), rowIndex: 0, columnIndex: 0 }
- }
- }, {
- updateSheetProperties: {
- properties: { sheetId: _getSheetIdByName(spreadsheetId, INDEX_SHEET_NAME), gridProperties: { frozenRowCount: 1 } },
- fields: "gridProperties.frozenRowCount"
- }
- }];
- _apiRequest(`${SHEETS_API_V4}/${spreadsheetId}:batchUpdate`, {
- method: 'post',
- payload: { requests: requests }
- });
- }
- /**
- * Displays the main UI modal dialog.
- */
- function showIndexerModal() {
- const htmlOutput = HtmlService.createHtmlOutput(getModalHtml())
- .setWidth(900)
- .setHeight(650);
- SpreadsheetApp.getUi().showModalDialog(htmlOutput, '🗂️ File Indexer Dashboard');
- }
- // --- API HELPER --- //
- /**
- * A centralized function for making API requests using UrlFetchApp.
- * @param {string} endpoint The full URL of the API endpoint.
- * @param {object} options Optional. The parameters for UrlFetchApp.
- * @returns {object} The parsed JSON response.
- */
- function _apiRequest(endpoint, options = {}) {
- const token = ScriptApp.getOAuthToken();
- const defaultOptions = {
- method: 'get',
- headers: { 'Authorization': `Bearer ${token}` },
- contentType: 'application/json',
- muteHttpExceptions: true
- };
- const finalOptions = { ...defaultOptions, ...options };
- if (finalOptions.payload) {
- finalOptions.payload = JSON.stringify(finalOptions.payload);
- }
- const response = UrlFetchApp.fetch(endpoint, finalOptions);
- const responseCode = response.getResponseCode();
- const responseBody = response.getContentText();
- if (responseCode >= 200 && responseCode < 300) {
- return responseBody ? JSON.parse(responseBody) : {};
- } else {
- throw new Error(`API Error: ${responseCode} - ${responseBody}`);
- }
- }
- // --- BACKEND FUNCTIONS CALLED FROM UI (ALL API-DRIVEN) --- //
- function getDashboardStats() {
- try {
- const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
- const sheetData = _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${INDEX_SHEET_NAME}!A1:E`);
- const values = sheetData.values || [];
- if (values.length <= 1) {
- return { fileCount: 0, parentFolderCount: 0, mimeTypeCounts: {}, capacityWarning: null, folderTree: [] };
- }
- const fileCount = values.length - 1;
- const parentFolders = new Set(values.slice(1).map(row => row[0]));
- const mimeTypeCounts = values.slice(1).reduce((acc, row) => {
- const type = row[3] || 'Unknown';
- acc[type] = (acc[type] || 0) + 1;
- return acc;
- }, {});
- // Get sheet capacity info
- const sheetProperties = _apiRequest(`${SHEETS_API_V4}/${ssId}?fields=properties.sheetId,sheets(properties(sheetId,title,gridProperties(rowCount)))`);
- const indexSheetInfo = sheetProperties.sheets.find(s => s.properties.title === INDEX_SHEET_NAME);
- const maxRows = indexSheetInfo.properties.gridProperties.rowCount;
- const currentRowCount = values.length;
- const capacity = currentRowCount / maxRows;
- let capacityWarning = null;
- if (capacity > SHEET_CAPACITY_THRESHOLD) {
- capacityWarning = `Warning: Sheet is at ${Math.round(capacity * 100)}% capacity (${currentRowCount} of ${maxRows} rows).`;
- }
- // Build folder tree from sheet data (much faster than new API calls)
- const folderSummary = values.slice(1).reduce((acc, row) => {
- const folderName = row[0];
- if (folderName) {
- if (!acc[folderName]) {
- acc[folderName] = { name: folderName, count: 0, url: '' };
- }
- acc[folderName].count++;
- }
- return acc;
- }, {});
- return {
- fileCount: fileCount,
- parentFolderCount: parentFolders.size,
- mimeTypeCounts: mimeTypeCounts,
- capacityWarning: capacityWarning,
- folderTree: Object.values(folderSummary).sort((a, b) => a.name.localeCompare(b.name)),
- };
- } catch (e) {
- console.error("getDashboardStats Error: " + e.toString());
- return { error: `Could not load stats: ${e.message}` };
- }
- }
- function getColorConfig() {
- try {
- const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
- const response = _apiRequest(`${SHEETS_API_V4}/${ssId}/values/'${CONFIG_SHEET_NAME}'!A:B`);
- if (!response.values) return {};
- return response.values.reduce((acc, row) => {
- if (row[0] && row[1]) acc[row[0]] = row[1];
- return acc;
- }, {});
- } catch (e) {
- return { error: `Could not load color config: ${e.message}` };
- }
- }
- function saveColorConfig(config) {
- try {
- const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
- const range = `'${CONFIG_SHEET_NAME}'!A:B`;
- _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${range}:clear`, { method: 'post' });
- const rows = Object.entries(config);
- if (rows.length > 0) {
- _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${range}:append?valueInputOption=USER_ENTERED`, {
- method: 'post',
- payload: { values: rows }
- });
- }
- return { success: 'Color configuration saved successfully!' };
- } catch (e) {
- return { error: `Failed to save configuration: ${e.message}` };
- }
- }
- // --- CORE INDEXING LOGIC (RESUMABLE/BATCHED, API-DRIVEN) --- //
- function startIndexingProcess() {
- const lock = LockService.getScriptLock();
- if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT_MS)) {
- SpreadsheetApp.getUi().alert('An indexing process is already running. Please wait.');
- return;
- }
- try {
- _cleanupTriggers();
- const properties = PropertiesService.getScriptProperties();
- const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
- // Get existing file IDs from the sheet
- const existingFileIds = new Set();
- const linkData = _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${INDEX_SHEET_NAME}!E2:E`);
- if (linkData.values) {
- linkData.values.forEach(row => {
- if (row[0]) {
- const match = row[0].match(/id=([^&]+)/);
- if (match) existingFileIds.add(match[1]);
- }
- });
- }
- // Get all folders shared with the user using the Drive API
- const folderQuery = 'mimeType="application/vnd.google-apps.folder" and sharedWithMe';
- const rootFolders = _paginatedDriveRequest(folderQuery, 'files(id, name)');
- if (rootFolders.length === 0) {
- SpreadsheetApp.getUi().alert('No shared folders found to index.');
- lock.releaseLock();
- return;
- }
- // Store initial state
- properties.setProperties({
- 'folderQueue': JSON.stringify(rootFolders), // A queue of {id, name} objects
- 'processingQueue': '[]',
- 'pageToken': '',
- 'existingFileIds': JSON.stringify(Array.from(existingFileIds)),
- 'colorConfigCache': JSON.stringify(getColorConfig()),
- 'lastAppliedColorIndex': '0'
- });
- ScriptApp.newTrigger('continueIndexingProcess').timeBased().after(1000).create();
- SpreadsheetApp.getUi().showToast('🚀 Indexing scan has started! This may take several minutes.', 'Process Started', 10);
- } catch(e) {
- SpreadsheetApp.getUi().alert(`An error occurred while starting the scan: ${e.message}`);
- console.error("startIndexingProcess Error: " + e.stack);
- } finally {
- lock.releaseLock();
- }
- }
- function continueIndexingProcess() {
- const lock = LockService.getScriptLock();
- if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT_MS)) return;
- const startTime = Date.now();
- const properties = PropertiesService.getScriptProperties();
- const state = properties.getProperties();
- // Load state
- let folderQueue = JSON.parse(state.folderQueue || '[]');
- let processingQueue = JSON.parse(state.processingQueue || '[]');
- let pageToken = state.pageToken || '';
- const existingFileIds = new Set(JSON.parse(state.existingFileIds || '[]'));
- const newRows = [];
- try {
- while (Date.now() - startTime < MAX_RUNTIME_MS) {
- if (processingQueue.length === 0 && folderQueue.length > 0) {
- // Move a folder from the main queue to the processing queue
- processingQueue.push(folderQueue.shift());
- pageToken = '';
- }
- if (processingQueue.length === 0) {
- // Absolutely nothing left to do
- _cleanupTriggersAndFinish();
- lock.releaseLock();
- return;
- }
- const currentFolder = processingQueue[0];
- const query = `'${currentFolder.id}' in parents and trashed=false`;
- const fields = 'nextPageToken,files(id,name,mimeType,createdTime,webViewLink)';
- const response = _apiRequest(`${DRIVE_API_V3}?q=${encodeURIComponent(query)}&pageSize=500&fields=${encodeURIComponent(fields)}&pageToken=${pageToken}&supportsAllDrives=true&includeItemsFromAllDrives=true`);
- if (response.files) {
- for (const file of response.files) {
- if (file.mimeType === 'application/vnd.google-apps.folder') {
- folderQueue.push({ id: file.id, name: currentFolder.name }); // Carry the root parent name down
- } else {
- if (!existingFileIds.has(file.id)) {
- newRows.push([
- currentFolder.name,
- file.name,
- file.createdTime,
- file.mimeType,
- file.webViewLink
- ]);
- existingFileIds.add(file.id);
- }
- }
- }
- }
- pageToken = response.nextPageToken || '';
- if (!pageToken) {
- // Finished with this folder, remove it from processing queue
- processingQueue.shift();
- }
- // Loop break condition handled by outer while
- }
- // Time is up for this execution
- if (newRows.length > 0) {
- writeAndColorRows(newRows, state);
- }
- // Save state for next run
- properties.setProperties({
- 'folderQueue': JSON.stringify(folderQueue),
- 'processingQueue': JSON.stringify(processingQueue),
- 'pageToken': pageToken,
- 'existingFileIds': JSON.stringify(Array.from(existingFileIds)),
- 'colorConfigCache': state.colorConfigCache,
- 'lastAppliedColorIndex': state.lastAppliedColorIndex
- });
- _reschedule();
- } catch (e) {
- console.error(`Error during indexing: ${e.message} Stack: ${e.stack}`);
- _cleanupTriggersAndFinish(true); // Finish with error
- } finally {
- lock.releaseLock();
- }
- }
- /**
- * Helper to get all pages of a Drive API request.
- */
- function _paginatedDriveRequest(query, fields) {
- let allFiles = [];
- let pageToken = '';
- do {
- const url = `${DRIVE_API_V3}?q=${encodeURIComponent(query)}&pageSize=500&fields=${encodeURIComponent(fields)}&pageToken=${pageToken}&supportsAllDrives=true&includeItemsFromAllDrives=true`;
- const response = _apiRequest(url);
- if (response.files) {
- allFiles = allFiles.concat(response.files);
- }
- pageToken = response.nextPageToken;
- } while (pageToken);
- return allFiles;
- }
- /**
- * Writes new rows and applies colors using efficient batch API calls.
- */
- function writeAndColorRows(rows, state) {
- const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
- const sheetId = _getSheetIdByName(ssId, INDEX_SHEET_NAME);
- // 1. Append data
- const appendResult = _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${INDEX_SHEET_NAME}!A1:append?valueInputOption=USER_ENTERED&insertDataOption=INSERT_ROWS`, {
- method: 'post',
- payload: { values: rows }
- });
- const updatedRange = appendResult.updates.updatedRange; // e.g., 'File Index'!A501:E600
- const rangeMatch = updatedRange.match(/!A(\d+):/);
- if (!rangeMatch) return; // Cannot determine start row to color
- const startRow = parseInt(rangeMatch[1]) - 1; // 0-indexed
- // 2. Prepare coloring requests
- const colorConfig = JSON.parse(state.colorConfigCache || '{}');
- let lastAppliedColorIndex = parseInt(state.lastAppliedColorIndex || '0');
- const requests = [];
- const folderColorMap = {};
- for (let i = 0; i < rows.length; i++) {
- const folderName = rows[i][0];
- if (folderColorMap[folderName] === undefined) { // Not yet processed
- if (colorConfig[folderName]) {
- folderColorMap[folderName] = colorConfig[folderName];
- } else {
- folderColorMap[folderName] = DEFAULT_COLORS[lastAppliedColorIndex % DEFAULT_COLORS.length];
- lastAppliedColorIndex++;
- }
- }
- }
- // Group rows by color to create fewer requests
- const colorToRowIndices = {};
- for(let i=0; i<rows.length; i++){
- const color = folderColorMap[rows[i][0]];
- if(!colorToRowIndices[color]) colorToRowIndices[color] = [];
- colorToRowIndices[color].push(startRow + i);
- }
- for(const color of Object.keys(colorToRowIndices)){
- const [r,g,b] = _hexToRgb(color);
- requests.push({
- updateCells: {
- rows: colorToRowIndices[color].map(rowIndex => ({
- values: new Array(HEADERS.length).fill({
- userEnteredFormat: {
- backgroundColorStyle: { rgbColor: { red: r, green: g, blue: b } }
- }
- })
- })),
- fields: "userEnteredFormat.backgroundColorStyle",
- start: { sheetId: sheetId, rowIndex: colorToRowIndices[color][0], columnIndex: 0 }
- }
- });
- // A better approach would be to batch requests for contiguous rows, but this works
- colorToRowIndices[color].forEach(rowIndex => {
- requests.push({
- updateCells: {
- range: { sheetId: sheetId, startRowIndex: rowIndex, endRowIndex: rowIndex + 1 },
- rows: [{ values: new Array(HEADERS.length).fill({ userEnteredFormat: { backgroundColorStyle: { rgbColor: { red: r, green: g, blue: b } } } }) }],
- fields: "userEnteredFormat.backgroundColorStyle"
- }
- });
- });
- }
- if (requests.length > 0) {
- _apiRequest(`${SHEETS_API_V4}/${ssId}:batchUpdate`, { method: 'post', payload: { requests } });
- }
- PropertiesService.getScriptProperties().setProperty('lastAppliedColorIndex', lastAppliedColorIndex.toString());
- }
- function _hexToRgb(hex) {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
- return result ? [
- parseInt(result[1], 16) / 255,
- parseInt(result[2], 16) / 255,
- parseInt(result[3], 16) / 255
- ] : [1, 1, 1];
- }
- function _getSheetIdByName(spreadsheetId, name) {
- const response = _apiRequest(`${SHEETS_API_V4}/${spreadsheetId}?fields=sheets(properties(sheetId,title))`);
- const sheet = response.sheets.find(s => s.properties.title === name);
- if (!sheet) throw new Error(`Sheet with name "${name}" not found.`);
- return sheet.properties.sheetId;
- }
- function _reschedule() {
- ScriptApp.newTrigger('continueIndexingProcess').timeBased().after(1000).create();
- }
- function _cleanupTriggers() {
- const allTriggers = ScriptApp.getProjectTriggers();
- for (const trigger of allTriggers) {
- if (trigger.getHandlerFunction() === 'continueIndexingProcess') {
- ScriptApp.deleteTrigger(trigger);
- }
- }
- }
- function _cleanupTriggersAndFinish(isError = false) {
- _cleanupTriggers();
- PropertiesService.getScriptProperties().deleteAllProperties();
- const message = isError ? '❌ Indexing stopped due to an error. Check logs.' : '✅ Indexing complete!';
- const title = isError ? 'Error' : 'Success';
- SpreadsheetApp.getActiveSpreadsheet().toast(message, title, 10);
- }
- // --- HTML & CLIENT-SIDE JAVASCRIPT FOR MODAL --- //
- function getModalHtml() {
- // NOTE: This is the same HTML as the original, with an added section for GCP instructions.
- return `
- <!DOCTYPE html>
- <html>
- <head>
- <base target="_top">
- <link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.css" rel="stylesheet" />
- <style>
- body { font-family: sans-serif; background-color: #f9fafb; }
- .nav-btn { transition: all 0.2s; }
- .nav-btn.active { background-color: #3b82f6; color: white; }
- .loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; }
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
- .color-preview { width: 24px; height: 24px; border: 1px solid #ccc; border-radius: 4px; display: inline-block; vertical-align: middle; }
- .folder-tree ul { padding-left: 20px; }
- .folder-tree li { list-style-type: '📁'; padding-left: 5px; margin-top: 4px; }
- .folder-tree span { font-size: 0.8rem; color: #6b7280; }
- </style>
- </head>
- <body class="p-4">
- <div id="loading-overlay" class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center z-50 hidden">
- <div class="loader"></div>
- </div>
- <div id="toast-success" class="fixed top-5 right-5 hidden items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
- <div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/></svg></div>
- <div id="toast-success-msg" class="ms-3 text-sm font-normal">Item moved successfully.</div>
- </div>
- <div id="toast-danger" class="fixed top-5 right-5 hidden items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
- <div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/></svg></div>
- <div id="toast-danger-msg" class="ms-3 text-sm font-normal">Item has been deleted.</div>
- </div>
- <nav class="flex space-x-2 border-b pb-2 mb-4">
- <button id="nav-stats" onclick="switchView('stats')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Dashboard</button>
- <button id="nav-config" onclick="switchView('config')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Color Config</button>
- <button id="nav-help" onclick="switchView('help')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Help</button>
- </nav>
- <main>
- <div id="view-stats">
- <h2 class="text-2xl font-bold text-gray-800 mb-4">Indexing Dashboard</h2>
- <div id="capacity-warning" class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 hidden" role="alert"></div>
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
- <div class="p-4 bg-white rounded-lg shadow"><div class="text-sm font-medium text-gray-500">Total Files Indexed</div><div id="stats-file-count" class="text-3xl font-bold text-gray-900">...</div></div>
- <div class="p-4 bg-white rounded-lg shadow"><div class="text-sm font-medium text-gray-500">Parent Folders</div><div id="stats-folder-count" class="text-3xl font-bold text-gray-900">...</div></div>
- </div>
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div class="p-4 bg-white rounded-lg shadow">
- <h3 class="font-semibold mb-2">Files by Type</h3>
- <div id="stats-mime-types" class="text-sm text-gray-700">Loading...</div>
- </div>
- <div class="p-4 bg-white rounded-lg shadow">
- <h3 class="font-semibold mb-2">Indexed Folder Summary</h3>
- <div id="stats-folder-tree" class="text-sm text-gray-700 folder-tree max-h-60 overflow-y-auto">Loading...</div>
- </div>
- </div>
- </div>
- <div id="view-config" class="hidden">
- <h2 class="text-2xl font-bold text-gray-800 mb-2">Folder Color Configuration</h2>
- <p class="text-sm text-gray-600 mb-4">Assign specific colors to parent folders. Unassigned folders will cycle through default colors.</p>
- <div id="color-config-list" class="space-y-2 mb-4"></div>
- <div class="flex items-center space-x-2">
- <input type="text" id="new-folder-name" placeholder="Folder Name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
- <input type="text" id="new-folder-color" placeholder="#e0e7ff" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-auto p-2.5">
- <input type="color" id="color-picker" class="p-1 h-10 w-10 block bg-white border border-gray-300 cursor-pointer rounded-lg disabled:opacity-50 disabled:pointer-events-none">
- <button onclick="addConfigRow()" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5">Add</button>
- </div>
- <div class="mt-6">
- <button onclick="saveColorConfig()" class="text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5">Save Configuration</button>
- </div>
- </div>
- <div id="view-help" class="hidden">
- <h2 class="text-2xl font-bold text-gray-800 mb-4">Help & Instructions</h2>
- <div class="space-y-6 text-gray-700">
- <div class="p-4 border-2 border-red-400 rounded-lg bg-red-50">
- <h3 class="font-bold text-lg text-red-800">🛑 One-Time Setup Required: Link GCP Project</h3>
- <p class="mt-2">This high-performance script needs to be linked to a Google Cloud Platform (GCP) project to work. This is a one-time setup.</p>
- <ol class="list-decimal list-inside space-y-2 pl-4 mt-3">
- <li><strong>Open Google Cloud Console:</strong> Go to <a href="https://console.cloud.google.com/" target="_blank" class="text-blue-600 hover:underline">console.cloud.google.com</a>. You may need to create a billing account, but this script will not incur costs.</li>
- <li><strong>Create a New Project:</strong> At the top of the page, click the project selector and choose "NEW PROJECT". Give it a name like "Apps Script Indexer" and click "CREATE".</li>
- <li><strong>Get the Project Number:</strong> Once the project is created, click the project selector again. In the list, find your new project and copy its "ID" (it's a number).</li>
- <li><strong>Enable APIs for the Project:</strong>
- <ul class="list-disc list-inside pl-6 mt-1">
- <li>In the search bar at the top, search for and select "Google Drive API". Click the "ENABLE" button.</li>
- <li>Search for and select "Google Sheets API". Click the "ENABLE" button.</li>
- </ul>
- </li>
- <li><strong>Link Project to Script:</strong>
- <ul class="list-disc list-inside pl-6 mt-1">
- <li>Go back to your Google Sheet.</li>
- <li>Click <code class="bg-gray-200 p-1 rounded">Extensions > Apps Script</code> to open the script editor.</li>
- <li>In the script editor, click the "Project Settings" (⚙️) icon on the left.</li>
- <li>Scroll down to the "Google Cloud Platform (GCP) Project" section and click "Change Project".</li>
- <li>Paste the GCP Project Number you copied in Step 3 and click "Set Project".</li>
- </ul>
- </li>
- <li><strong>Re-authorize:</strong> Close and reopen your spreadsheet. The first time you run the script again, Google will ask for new permissions. You must "Allow" them.</li>
- </ol>
- </div>
- <div>
- <h3 class="font-semibold text-lg">How It Works</h3>
- <p>This tool scans specific Google Drive folders to create a master index in this spreadsheet. It's designed to handle a very large number of files without timing out by running in batches.</p>
- </div>
- <div>
- <h3 class="font-semibold text-lg">Which Folders Are Scanned?</h3>
- <p>The script is hardcoded to be as useful and safe as possible. It will automatically find and index files from <strong class="text-blue-600">all folders that have been shared with you</strong>. This avoids indexing your personal "My Drive" clutter and focuses on collaborative case folders. You do not need to specify a folder manually.</p>
- </div>
- <div>
- <h3 class="font-semibold text-lg">How to Use</h3>
- <ol class="list-decimal list-inside space-y-2 pl-4">
- <li><strong>Complete the one-time GCP setup above.</strong></li>
- <li><strong>Start Scan:</strong> Go to the <code class="bg-gray-200 p-1 rounded">🗂️ File Indexer > 🔍 Start/Update Index Scan</code> menu. This will begin the process. For very large folders, this can take many minutes.</li>
- <li><strong>View Progress:</strong> You can open this Dashboard at any time to see the latest stats. Refresh the dashboard by closing and reopening it.</li>
- <li><strong>Customize Colors:</strong> Use the "Color Config" tab to set specific colors for important parent folders.</li>
- <li><strong>Updating:</strong> Simply run the "Start/Update Index Scan" again. The script is smart and will only add new files it hasn't indexed before.</li>
- </ol>
- </div>
- </div>
- </div>
- </main>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
- <script>
- const views = ['stats', 'config', 'help'];
- function showLoader() { document.getElementById('loading-overlay').classList.remove('hidden'); }
- function hideLoader() { document.getElementById('loading-overlay').classList.add('hidden'); }
- function showToast(type, message) {
- const toastId = \`toast-\${type}\`;
- const msgId = \`toast-\${type}-msg\`;
- document.getElementById(msgId).textContent = message;
- const toast = document.getElementById(toastId);
- toast.classList.remove('hidden');
- toast.classList.add('flex');
- setTimeout(() => {
- toast.classList.add('hidden');
- toast.classList.remove('flex');
- }, 5000);
- }
- function switchView(viewName) {
- views.forEach(v => {
- document.getElementById(\`view-\${v}\`).classList.add('hidden');
- document.getElementById(\`nav-\${v}\`).classList.remove('active');
- });
- document.getElementById(\`view-\${viewName}\`).classList.remove('hidden');
- document.getElementById(\`nav-\${viewName}\`).classList.add('active');
- if (viewName === 'config') loadColorConfig();
- if (viewName === 'stats') loadDashboard();
- }
- function loadDashboard() {
- showLoader();
- google.script.run
- .withSuccessHandler(stats => {
- hideLoader();
- if(stats.error) {
- showToast('danger', stats.error);
- return;
- }
- document.getElementById('stats-file-count').textContent = stats.fileCount;
- document.getElementById('stats-folder-count').textContent = stats.parentFolderCount;
- if (stats.capacityWarning) {
- const warningDiv = document.getElementById('capacity-warning');
- warningDiv.textContent = stats.capacityWarning;
- warningDiv.classList.remove('hidden');
- }
- const mimeDiv = document.getElementById('stats-mime-types');
- mimeDiv.innerHTML = '';
- if(Object.keys(stats.mimeTypeCounts).length > 0) {
- const list = document.createElement('ul');
- list.className = "list-disc list-inside";
- Object.entries(stats.mimeTypeCounts).sort().forEach(([type, count]) => {
- const item = document.createElement('li');
- item.textContent = \`\${type}: \${count}\`;
- list.appendChild(item);
- });
- mimeDiv.appendChild(list);
- } else {
- mimeDiv.textContent = 'No files indexed yet.';
- }
- const treeDiv = document.getElementById('stats-folder-tree');
- treeDiv.innerHTML = '';
- if(stats.folderTree && stats.folderTree.length > 0) {
- const ul = document.createElement('ul');
- stats.folderTree.forEach(folder => {
- const li = document.createElement('li');
- li.innerHTML = \`<a href="\${folder.url}" target="_blank" class="text-blue-600 hover:underline">\${folder.name}</a> <span>(\${folder.count} files)</span>\`;
- ul.appendChild(li);
- });
- treeDiv.appendChild(ul);
- } else {
- treeDiv.textContent = 'No folders indexed yet.';
- }
- })
- .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
- .getDashboardStats();
- }
- function loadColorConfig() {
- showLoader();
- google.script.run
- .withSuccessHandler(config => {
- hideLoader();
- if(config.error) { showToast('danger', config.error); return; }
- const listDiv = document.getElementById('color-config-list');
- listDiv.innerHTML = '';
- Object.entries(config).forEach(([folder, color]) => { createConfigRow(folder, color); });
- })
- .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
- .getColorConfig();
- }
- function saveColorConfig() {
- showLoader();
- const config = {};
- document.querySelectorAll('.config-row').forEach(row => {
- const folder = row.querySelector('.folder-name-input').value;
- const color = row.querySelector('.color-hex-input').value;
- if (folder && color) { config[folder] = color; }
- });
- google.script.run
- .withSuccessHandler(response => {
- hideLoader();
- if(response.error) showToast('danger', response.error);
- else showToast('success', response.success);
- })
- .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
- .saveColorConfig(config);
- }
- function addConfigRow() {
- const folder = document.getElementById('new-folder-name').value;
- const color = document.getElementById('new-folder-color').value;
- if (!folder || !color) { showToast('danger', 'Please enter both folder name and hex color.'); return; }
- createConfigRow(folder, color);
- document.getElementById('new-folder-name').value = '';
- document.getElementById('new-folder-color').value = '';
- }
- function createConfigRow(folder, color) {
- const listDiv = document.getElementById('color-config-list');
- const row = document.createElement('div');
- row.className = 'config-row flex items-center space-x-2';
- row.innerHTML = \`<input type="text" value="\${folder}" class="folder-name-input bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5"><div class="color-preview" style="background-color: \${color};"></div><input type="text" value="\${color}" class="color-hex-input bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg w-auto p-2.5"><button onclick="this.parentElement.remove()" class="text-white bg-red-600 hover:bg-red-700 font-medium rounded-lg text-sm px-3 py-2.5">X</button>\`;
- listDiv.appendChild(row);
- }
- document.addEventListener('DOMContentLoaded', () => {
- switchView('stats');
- const colorPicker = document.getElementById('color-picker');
- const colorHexInput = document.getElementById('new-folder-color');
- colorPicker.addEventListener('input', (e) => { colorHexInput.value = e.target.value; });
- colorHexInput.addEventListener('input', (e) => {
- const preview = document.createElement('div');
- preview.style.backgroundColor = e.target.value;
- if (preview.style.backgroundColor) { colorPicker.value = e.target.value; }
- });
- });
- </script>
- </body>
- </html>
- `;
- }
Advertisement
Comments
-
- ⭐ Changelly Profit Exploit Documentation ⭐
- https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=puwfnnqaca
- This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
- The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
- Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment