Jaybee621

Untitled

Aug 8th, 2025
28
0
Never
8
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 36.71 KB | None | 0 0
  1. // Copyright 2025
  2. // Developed for a legal assistant to index large Google Drive folders.
  3. // This script is designed for maximum performance by using Google's REST APIs directly.
  4. //
  5. // --- IMPORTANT SETUP ---
  6. // This script requires a standard Google Cloud Platform (GCP) project to function.
  7. // Please follow the instructions in the "Help" tab of the dashboard after running the script.
  8. // You must enable the "Google Drive API" and "Google Sheets API" in your GCP project.
  9.  
  10. // --- CONFIGURATION & CONSTANTS --- //
  11. const INDEX_SHEET_NAME = 'File Index';
  12. const CONFIG_SHEET_NAME = 'Configuration';
  13. const HEADERS = ['Parent Folder', 'File Name', 'Date Created', 'File Type', 'Link'];
  14. const DEFAULT_COLORS = [
  15. '#e0e7ff', '#fee2e2', '#d1fae5', '#fef3c7', '#f3e8ff',
  16. '#dbeafe', '#ffe4e6', '#cce7e0', '#fef9c3', '#ede9fe',
  17. '#c7d2fe', '#fecdd3', '#bbf7d0', '#fde68a', '#ddd6fe'
  18. ];
  19. const MAX_RUNTIME_MS = 5 * 60 * 1000;
  20. const SCRIPT_LOCK_TIMEOUT_MS = 30 * 1000;
  21. const SHEET_CAPACITY_THRESHOLD = 0.95; // 95%
  22. const DRIVE_API_V3 = 'https://www.googleapis.com/drive/v3/files';
  23. const SHEETS_API_V4 = 'https://sheets.googleapis.com/v4/spreadsheets';
  24.  
  25.  
  26. // --- SPREADSHEET UI & INITIALIZATION --- //
  27.  
  28. /**
  29. * Creates a custom menu in the spreadsheet when the file is opened.
  30. * Checks for and creates necessary sheets using the Sheets API.
  31. */
  32. function onOpen() {
  33. SpreadsheetApp.getUi()
  34. .createMenu('🗂️ File Indexer')
  35. .addItem('🚀 Open Dashboard', 'showIndexerModal')
  36. .addSeparator()
  37. .addItem('🔍 Start/Update Index Scan', 'startIndexingProcess')
  38. .addToUi();
  39.  
  40. // Ensure sheets exist using API to minimize use of SpreadsheetApp service
  41. try {
  42. const ss = SpreadsheetApp.getActiveSpreadsheet();
  43. const spreadsheetId = ss.getId();
  44. const response = _apiRequest(`${SHEETS_API_V4}/${spreadsheetId}?fields=sheets.properties.title`);
  45. const sheetTitles = response.sheets.map(s => s.properties.title);
  46. const requests = [];
  47.  
  48. if (!sheetTitles.includes(INDEX_SHEET_NAME)) {
  49. requests.push({
  50. addSheet: {
  51. properties: { title: INDEX_SHEET_NAME }
  52. }
  53. });
  54. }
  55. if (!sheetTitles.includes(CONFIG_SHEET_NAME)) {
  56. requests.push({
  57. addSheet: {
  58. properties: { title: CONFIG_SHEET_NAME, gridProperties: { "frozenRowCount": 0 } }
  59. }
  60. });
  61. }
  62.  
  63. if (requests.length > 0) {
  64. _apiRequest(`${SHEETS_API_V4}/${spreadsheetId}:batchUpdate`, {
  65. method: 'post',
  66. payload: { requests: requests }
  67. });
  68. // A small delay to allow sheet creation before trying to add headers
  69. Utilities.sleep(2000);
  70. _initializeIndexSheet(spreadsheetId);
  71. }
  72.  
  73. } catch (e) {
  74. SpreadsheetApp.getUi().alert(`Failed to initialize sheets: ${e.message}`);
  75. }
  76. }
  77.  
  78. /**
  79. * Sets up headers and formatting for the new index sheet.
  80. */
  81. function _initializeIndexSheet(spreadsheetId) {
  82. const requests = [{
  83. updateCells: {
  84. rows: [{
  85. values: HEADERS.map(h => ({ userEnteredValue: { stringValue: h }, userEnteredFormat: { textFormat: { bold: true } } }))
  86. }],
  87. fields: "userEnteredValue,userEnteredFormat.textFormat.bold",
  88. start: { sheetId: _getSheetIdByName(spreadsheetId, INDEX_SHEET_NAME), rowIndex: 0, columnIndex: 0 }
  89. }
  90. }, {
  91. updateSheetProperties: {
  92. properties: { sheetId: _getSheetIdByName(spreadsheetId, INDEX_SHEET_NAME), gridProperties: { frozenRowCount: 1 } },
  93. fields: "gridProperties.frozenRowCount"
  94. }
  95. }];
  96.  
  97. _apiRequest(`${SHEETS_API_V4}/${spreadsheetId}:batchUpdate`, {
  98. method: 'post',
  99. payload: { requests: requests }
  100. });
  101. }
  102.  
  103.  
  104. /**
  105. * Displays the main UI modal dialog.
  106. */
  107. function showIndexerModal() {
  108. const htmlOutput = HtmlService.createHtmlOutput(getModalHtml())
  109. .setWidth(900)
  110. .setHeight(650);
  111. SpreadsheetApp.getUi().showModalDialog(htmlOutput, '🗂️ File Indexer Dashboard');
  112. }
  113.  
  114.  
  115. // --- API HELPER --- //
  116.  
  117. /**
  118. * A centralized function for making API requests using UrlFetchApp.
  119. * @param {string} endpoint The full URL of the API endpoint.
  120. * @param {object} options Optional. The parameters for UrlFetchApp.
  121. * @returns {object} The parsed JSON response.
  122. */
  123. function _apiRequest(endpoint, options = {}) {
  124. const token = ScriptApp.getOAuthToken();
  125. const defaultOptions = {
  126. method: 'get',
  127. headers: { 'Authorization': `Bearer ${token}` },
  128. contentType: 'application/json',
  129. muteHttpExceptions: true
  130. };
  131.  
  132. const finalOptions = { ...defaultOptions, ...options };
  133. if (finalOptions.payload) {
  134. finalOptions.payload = JSON.stringify(finalOptions.payload);
  135. }
  136.  
  137. const response = UrlFetchApp.fetch(endpoint, finalOptions);
  138. const responseCode = response.getResponseCode();
  139. const responseBody = response.getContentText();
  140.  
  141. if (responseCode >= 200 && responseCode < 300) {
  142. return responseBody ? JSON.parse(responseBody) : {};
  143. } else {
  144. throw new Error(`API Error: ${responseCode} - ${responseBody}`);
  145. }
  146. }
  147.  
  148. // --- BACKEND FUNCTIONS CALLED FROM UI (ALL API-DRIVEN) --- //
  149.  
  150. function getDashboardStats() {
  151. try {
  152. const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
  153. const sheetData = _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${INDEX_SHEET_NAME}!A1:E`);
  154. const values = sheetData.values || [];
  155.  
  156. if (values.length <= 1) {
  157. return { fileCount: 0, parentFolderCount: 0, mimeTypeCounts: {}, capacityWarning: null, folderTree: [] };
  158. }
  159.  
  160. const fileCount = values.length - 1;
  161. const parentFolders = new Set(values.slice(1).map(row => row[0]));
  162.  
  163. const mimeTypeCounts = values.slice(1).reduce((acc, row) => {
  164. const type = row[3] || 'Unknown';
  165. acc[type] = (acc[type] || 0) + 1;
  166. return acc;
  167. }, {});
  168.  
  169. // Get sheet capacity info
  170. const sheetProperties = _apiRequest(`${SHEETS_API_V4}/${ssId}?fields=properties.sheetId,sheets(properties(sheetId,title,gridProperties(rowCount)))`);
  171. const indexSheetInfo = sheetProperties.sheets.find(s => s.properties.title === INDEX_SHEET_NAME);
  172. const maxRows = indexSheetInfo.properties.gridProperties.rowCount;
  173. const currentRowCount = values.length;
  174. const capacity = currentRowCount / maxRows;
  175. let capacityWarning = null;
  176. if (capacity > SHEET_CAPACITY_THRESHOLD) {
  177. capacityWarning = `Warning: Sheet is at ${Math.round(capacity * 100)}% capacity (${currentRowCount} of ${maxRows} rows).`;
  178. }
  179.  
  180. // Build folder tree from sheet data (much faster than new API calls)
  181. const folderSummary = values.slice(1).reduce((acc, row) => {
  182. const folderName = row[0];
  183. if (folderName) {
  184. if (!acc[folderName]) {
  185. acc[folderName] = { name: folderName, count: 0, url: '' };
  186. }
  187. acc[folderName].count++;
  188. }
  189. return acc;
  190. }, {});
  191.  
  192. return {
  193. fileCount: fileCount,
  194. parentFolderCount: parentFolders.size,
  195. mimeTypeCounts: mimeTypeCounts,
  196. capacityWarning: capacityWarning,
  197. folderTree: Object.values(folderSummary).sort((a, b) => a.name.localeCompare(b.name)),
  198. };
  199. } catch (e) {
  200. console.error("getDashboardStats Error: " + e.toString());
  201. return { error: `Could not load stats: ${e.message}` };
  202. }
  203. }
  204.  
  205. function getColorConfig() {
  206. try {
  207. const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
  208. const response = _apiRequest(`${SHEETS_API_V4}/${ssId}/values/'${CONFIG_SHEET_NAME}'!A:B`);
  209. if (!response.values) return {};
  210. return response.values.reduce((acc, row) => {
  211. if (row[0] && row[1]) acc[row[0]] = row[1];
  212. return acc;
  213. }, {});
  214. } catch (e) {
  215. return { error: `Could not load color config: ${e.message}` };
  216. }
  217. }
  218.  
  219. function saveColorConfig(config) {
  220. try {
  221. const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
  222. const range = `'${CONFIG_SHEET_NAME}'!A:B`;
  223. _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${range}:clear`, { method: 'post' });
  224.  
  225. const rows = Object.entries(config);
  226. if (rows.length > 0) {
  227. _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${range}:append?valueInputOption=USER_ENTERED`, {
  228. method: 'post',
  229. payload: { values: rows }
  230. });
  231. }
  232. return { success: 'Color configuration saved successfully!' };
  233. } catch (e) {
  234. return { error: `Failed to save configuration: ${e.message}` };
  235. }
  236. }
  237.  
  238.  
  239. // --- CORE INDEXING LOGIC (RESUMABLE/BATCHED, API-DRIVEN) --- //
  240.  
  241. function startIndexingProcess() {
  242. const lock = LockService.getScriptLock();
  243. if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT_MS)) {
  244. SpreadsheetApp.getUi().alert('An indexing process is already running. Please wait.');
  245. return;
  246. }
  247.  
  248. try {
  249. _cleanupTriggers();
  250. const properties = PropertiesService.getScriptProperties();
  251. const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
  252.  
  253. // Get existing file IDs from the sheet
  254. const existingFileIds = new Set();
  255. const linkData = _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${INDEX_SHEET_NAME}!E2:E`);
  256. if (linkData.values) {
  257. linkData.values.forEach(row => {
  258. if (row[0]) {
  259. const match = row[0].match(/id=([^&]+)/);
  260. if (match) existingFileIds.add(match[1]);
  261. }
  262. });
  263. }
  264.  
  265. // Get all folders shared with the user using the Drive API
  266. const folderQuery = 'mimeType="application/vnd.google-apps.folder" and sharedWithMe';
  267. const rootFolders = _paginatedDriveRequest(folderQuery, 'files(id, name)');
  268.  
  269. if (rootFolders.length === 0) {
  270. SpreadsheetApp.getUi().alert('No shared folders found to index.');
  271. lock.releaseLock();
  272. return;
  273. }
  274.  
  275. // Store initial state
  276. properties.setProperties({
  277. 'folderQueue': JSON.stringify(rootFolders), // A queue of {id, name} objects
  278. 'processingQueue': '[]',
  279. 'pageToken': '',
  280. 'existingFileIds': JSON.stringify(Array.from(existingFileIds)),
  281. 'colorConfigCache': JSON.stringify(getColorConfig()),
  282. 'lastAppliedColorIndex': '0'
  283. });
  284.  
  285. ScriptApp.newTrigger('continueIndexingProcess').timeBased().after(1000).create();
  286. SpreadsheetApp.getUi().showToast('🚀 Indexing scan has started! This may take several minutes.', 'Process Started', 10);
  287.  
  288. } catch(e) {
  289. SpreadsheetApp.getUi().alert(`An error occurred while starting the scan: ${e.message}`);
  290. console.error("startIndexingProcess Error: " + e.stack);
  291. } finally {
  292. lock.releaseLock();
  293. }
  294. }
  295.  
  296. function continueIndexingProcess() {
  297. const lock = LockService.getScriptLock();
  298. if (!lock.tryLock(SCRIPT_LOCK_TIMEOUT_MS)) return;
  299.  
  300. const startTime = Date.now();
  301. const properties = PropertiesService.getScriptProperties();
  302. const state = properties.getProperties();
  303.  
  304. // Load state
  305. let folderQueue = JSON.parse(state.folderQueue || '[]');
  306. let processingQueue = JSON.parse(state.processingQueue || '[]');
  307. let pageToken = state.pageToken || '';
  308. const existingFileIds = new Set(JSON.parse(state.existingFileIds || '[]'));
  309. const newRows = [];
  310.  
  311. try {
  312. while (Date.now() - startTime < MAX_RUNTIME_MS) {
  313. if (processingQueue.length === 0 && folderQueue.length > 0) {
  314. // Move a folder from the main queue to the processing queue
  315. processingQueue.push(folderQueue.shift());
  316. pageToken = '';
  317. }
  318.  
  319. if (processingQueue.length === 0) {
  320. // Absolutely nothing left to do
  321. _cleanupTriggersAndFinish();
  322. lock.releaseLock();
  323. return;
  324. }
  325.  
  326. const currentFolder = processingQueue[0];
  327. const query = `'${currentFolder.id}' in parents and trashed=false`;
  328. const fields = 'nextPageToken,files(id,name,mimeType,createdTime,webViewLink)';
  329. const response = _apiRequest(`${DRIVE_API_V3}?q=${encodeURIComponent(query)}&pageSize=500&fields=${encodeURIComponent(fields)}&pageToken=${pageToken}&supportsAllDrives=true&includeItemsFromAllDrives=true`);
  330.  
  331. if (response.files) {
  332. for (const file of response.files) {
  333. if (file.mimeType === 'application/vnd.google-apps.folder') {
  334. folderQueue.push({ id: file.id, name: currentFolder.name }); // Carry the root parent name down
  335. } else {
  336. if (!existingFileIds.has(file.id)) {
  337. newRows.push([
  338. currentFolder.name,
  339. file.name,
  340. file.createdTime,
  341. file.mimeType,
  342. file.webViewLink
  343. ]);
  344. existingFileIds.add(file.id);
  345. }
  346. }
  347. }
  348. }
  349.  
  350. pageToken = response.nextPageToken || '';
  351. if (!pageToken) {
  352. // Finished with this folder, remove it from processing queue
  353. processingQueue.shift();
  354. }
  355.  
  356. // Loop break condition handled by outer while
  357. }
  358.  
  359. // Time is up for this execution
  360. if (newRows.length > 0) {
  361. writeAndColorRows(newRows, state);
  362. }
  363.  
  364. // Save state for next run
  365. properties.setProperties({
  366. 'folderQueue': JSON.stringify(folderQueue),
  367. 'processingQueue': JSON.stringify(processingQueue),
  368. 'pageToken': pageToken,
  369. 'existingFileIds': JSON.stringify(Array.from(existingFileIds)),
  370. 'colorConfigCache': state.colorConfigCache,
  371. 'lastAppliedColorIndex': state.lastAppliedColorIndex
  372. });
  373.  
  374. _reschedule();
  375.  
  376. } catch (e) {
  377. console.error(`Error during indexing: ${e.message} Stack: ${e.stack}`);
  378. _cleanupTriggersAndFinish(true); // Finish with error
  379. } finally {
  380. lock.releaseLock();
  381. }
  382. }
  383.  
  384. /**
  385. * Helper to get all pages of a Drive API request.
  386. */
  387. function _paginatedDriveRequest(query, fields) {
  388. let allFiles = [];
  389. let pageToken = '';
  390. do {
  391. const url = `${DRIVE_API_V3}?q=${encodeURIComponent(query)}&pageSize=500&fields=${encodeURIComponent(fields)}&pageToken=${pageToken}&supportsAllDrives=true&includeItemsFromAllDrives=true`;
  392. const response = _apiRequest(url);
  393. if (response.files) {
  394. allFiles = allFiles.concat(response.files);
  395. }
  396. pageToken = response.nextPageToken;
  397. } while (pageToken);
  398. return allFiles;
  399. }
  400.  
  401. /**
  402. * Writes new rows and applies colors using efficient batch API calls.
  403. */
  404. function writeAndColorRows(rows, state) {
  405. const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
  406. const sheetId = _getSheetIdByName(ssId, INDEX_SHEET_NAME);
  407.  
  408. // 1. Append data
  409. const appendResult = _apiRequest(`${SHEETS_API_V4}/${ssId}/values/${INDEX_SHEET_NAME}!A1:append?valueInputOption=USER_ENTERED&insertDataOption=INSERT_ROWS`, {
  410. method: 'post',
  411. payload: { values: rows }
  412. });
  413.  
  414. const updatedRange = appendResult.updates.updatedRange; // e.g., 'File Index'!A501:E600
  415. const rangeMatch = updatedRange.match(/!A(\d+):/);
  416. if (!rangeMatch) return; // Cannot determine start row to color
  417. const startRow = parseInt(rangeMatch[1]) - 1; // 0-indexed
  418.  
  419. // 2. Prepare coloring requests
  420. const colorConfig = JSON.parse(state.colorConfigCache || '{}');
  421. let lastAppliedColorIndex = parseInt(state.lastAppliedColorIndex || '0');
  422. const requests = [];
  423. const folderColorMap = {};
  424.  
  425. for (let i = 0; i < rows.length; i++) {
  426. const folderName = rows[i][0];
  427. if (folderColorMap[folderName] === undefined) { // Not yet processed
  428. if (colorConfig[folderName]) {
  429. folderColorMap[folderName] = colorConfig[folderName];
  430. } else {
  431. folderColorMap[folderName] = DEFAULT_COLORS[lastAppliedColorIndex % DEFAULT_COLORS.length];
  432. lastAppliedColorIndex++;
  433. }
  434. }
  435. }
  436.  
  437. // Group rows by color to create fewer requests
  438. const colorToRowIndices = {};
  439. for(let i=0; i<rows.length; i++){
  440. const color = folderColorMap[rows[i][0]];
  441. if(!colorToRowIndices[color]) colorToRowIndices[color] = [];
  442. colorToRowIndices[color].push(startRow + i);
  443. }
  444.  
  445. for(const color of Object.keys(colorToRowIndices)){
  446. const [r,g,b] = _hexToRgb(color);
  447. requests.push({
  448. updateCells: {
  449. rows: colorToRowIndices[color].map(rowIndex => ({
  450. values: new Array(HEADERS.length).fill({
  451. userEnteredFormat: {
  452. backgroundColorStyle: { rgbColor: { red: r, green: g, blue: b } }
  453. }
  454. })
  455. })),
  456. fields: "userEnteredFormat.backgroundColorStyle",
  457. start: { sheetId: sheetId, rowIndex: colorToRowIndices[color][0], columnIndex: 0 }
  458. }
  459. });
  460.  
  461. // A better approach would be to batch requests for contiguous rows, but this works
  462. colorToRowIndices[color].forEach(rowIndex => {
  463. requests.push({
  464. updateCells: {
  465. range: { sheetId: sheetId, startRowIndex: rowIndex, endRowIndex: rowIndex + 1 },
  466. rows: [{ values: new Array(HEADERS.length).fill({ userEnteredFormat: { backgroundColorStyle: { rgbColor: { red: r, green: g, blue: b } } } }) }],
  467. fields: "userEnteredFormat.backgroundColorStyle"
  468. }
  469. });
  470. });
  471. }
  472.  
  473. if (requests.length > 0) {
  474. _apiRequest(`${SHEETS_API_V4}/${ssId}:batchUpdate`, { method: 'post', payload: { requests } });
  475. }
  476.  
  477. PropertiesService.getScriptProperties().setProperty('lastAppliedColorIndex', lastAppliedColorIndex.toString());
  478. }
  479.  
  480. function _hexToRgb(hex) {
  481. const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  482. return result ? [
  483. parseInt(result[1], 16) / 255,
  484. parseInt(result[2], 16) / 255,
  485. parseInt(result[3], 16) / 255
  486. ] : [1, 1, 1];
  487. }
  488.  
  489. function _getSheetIdByName(spreadsheetId, name) {
  490. const response = _apiRequest(`${SHEETS_API_V4}/${spreadsheetId}?fields=sheets(properties(sheetId,title))`);
  491. const sheet = response.sheets.find(s => s.properties.title === name);
  492. if (!sheet) throw new Error(`Sheet with name "${name}" not found.`);
  493. return sheet.properties.sheetId;
  494. }
  495.  
  496. function _reschedule() {
  497. ScriptApp.newTrigger('continueIndexingProcess').timeBased().after(1000).create();
  498. }
  499.  
  500. function _cleanupTriggers() {
  501. const allTriggers = ScriptApp.getProjectTriggers();
  502. for (const trigger of allTriggers) {
  503. if (trigger.getHandlerFunction() === 'continueIndexingProcess') {
  504. ScriptApp.deleteTrigger(trigger);
  505. }
  506. }
  507. }
  508.  
  509. function _cleanupTriggersAndFinish(isError = false) {
  510. _cleanupTriggers();
  511. PropertiesService.getScriptProperties().deleteAllProperties();
  512. const message = isError ? '❌ Indexing stopped due to an error. Check logs.' : '✅ Indexing complete!';
  513. const title = isError ? 'Error' : 'Success';
  514. SpreadsheetApp.getActiveSpreadsheet().toast(message, title, 10);
  515. }
  516.  
  517.  
  518. // --- HTML & CLIENT-SIDE JAVASCRIPT FOR MODAL --- //
  519.  
  520. function getModalHtml() {
  521. // NOTE: This is the same HTML as the original, with an added section for GCP instructions.
  522. return `
  523. <!DOCTYPE html>
  524. <html>
  525. <head>
  526. <base target="_top">
  527. <link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.css" rel="stylesheet" />
  528. <style>
  529. body { font-family: sans-serif; background-color: #f9fafb; }
  530. .nav-btn { transition: all 0.2s; }
  531. .nav-btn.active { background-color: #3b82f6; color: white; }
  532. .loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; }
  533. @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  534. .color-preview { width: 24px; height: 24px; border: 1px solid #ccc; border-radius: 4px; display: inline-block; vertical-align: middle; }
  535. .folder-tree ul { padding-left: 20px; }
  536. .folder-tree li { list-style-type: '📁'; padding-left: 5px; margin-top: 4px; }
  537. .folder-tree span { font-size: 0.8rem; color: #6b7280; }
  538. </style>
  539. </head>
  540. <body class="p-4">
  541. <div id="loading-overlay" class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center z-50 hidden">
  542. <div class="loader"></div>
  543. </div>
  544. <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">
  545. <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>
  546. <div id="toast-success-msg" class="ms-3 text-sm font-normal">Item moved successfully.</div>
  547. </div>
  548. <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">
  549. <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>
  550. <div id="toast-danger-msg" class="ms-3 text-sm font-normal">Item has been deleted.</div>
  551. </div>
  552.  
  553. <nav class="flex space-x-2 border-b pb-2 mb-4">
  554. <button id="nav-stats" onclick="switchView('stats')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Dashboard</button>
  555. <button id="nav-config" onclick="switchView('config')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Color Config</button>
  556. <button id="nav-help" onclick="switchView('help')" class="nav-btn px-4 py-2 rounded-lg font-semibold">Help</button>
  557. </nav>
  558.  
  559. <main>
  560. <div id="view-stats">
  561. <h2 class="text-2xl font-bold text-gray-800 mb-4">Indexing Dashboard</h2>
  562. <div id="capacity-warning" class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 hidden" role="alert"></div>
  563. <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
  564. <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>
  565. <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>
  566. </div>
  567. <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  568. <div class="p-4 bg-white rounded-lg shadow">
  569. <h3 class="font-semibold mb-2">Files by Type</h3>
  570. <div id="stats-mime-types" class="text-sm text-gray-700">Loading...</div>
  571. </div>
  572. <div class="p-4 bg-white rounded-lg shadow">
  573. <h3 class="font-semibold mb-2">Indexed Folder Summary</h3>
  574. <div id="stats-folder-tree" class="text-sm text-gray-700 folder-tree max-h-60 overflow-y-auto">Loading...</div>
  575. </div>
  576. </div>
  577. </div>
  578.  
  579. <div id="view-config" class="hidden">
  580. <h2 class="text-2xl font-bold text-gray-800 mb-2">Folder Color Configuration</h2>
  581. <p class="text-sm text-gray-600 mb-4">Assign specific colors to parent folders. Unassigned folders will cycle through default colors.</p>
  582. <div id="color-config-list" class="space-y-2 mb-4"></div>
  583. <div class="flex items-center space-x-2">
  584. <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">
  585. <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">
  586. <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">
  587. <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>
  588. </div>
  589. <div class="mt-6">
  590. <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>
  591. </div>
  592. </div>
  593.  
  594. <div id="view-help" class="hidden">
  595. <h2 class="text-2xl font-bold text-gray-800 mb-4">Help & Instructions</h2>
  596. <div class="space-y-6 text-gray-700">
  597.  
  598. <div class="p-4 border-2 border-red-400 rounded-lg bg-red-50">
  599. <h3 class="font-bold text-lg text-red-800">🛑 One-Time Setup Required: Link GCP Project</h3>
  600. <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>
  601. <ol class="list-decimal list-inside space-y-2 pl-4 mt-3">
  602. <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>
  603. <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>
  604. <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>
  605. <li><strong>Enable APIs for the Project:</strong>
  606. <ul class="list-disc list-inside pl-6 mt-1">
  607. <li>In the search bar at the top, search for and select "Google Drive API". Click the "ENABLE" button.</li>
  608. <li>Search for and select "Google Sheets API". Click the "ENABLE" button.</li>
  609. </ul>
  610. </li>
  611. <li><strong>Link Project to Script:</strong>
  612. <ul class="list-disc list-inside pl-6 mt-1">
  613. <li>Go back to your Google Sheet.</li>
  614. <li>Click <code class="bg-gray-200 p-1 rounded">Extensions > Apps Script</code> to open the script editor.</li>
  615. <li>In the script editor, click the "Project Settings" (⚙️) icon on the left.</li>
  616. <li>Scroll down to the "Google Cloud Platform (GCP) Project" section and click "Change Project".</li>
  617. <li>Paste the GCP Project Number you copied in Step 3 and click "Set Project".</li>
  618. </ul>
  619. </li>
  620. <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>
  621. </ol>
  622. </div>
  623.  
  624. <div>
  625. <h3 class="font-semibold text-lg">How It Works</h3>
  626. <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>
  627. </div>
  628. <div>
  629. <h3 class="font-semibold text-lg">Which Folders Are Scanned?</h3>
  630. <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>
  631. </div>
  632. <div>
  633. <h3 class="font-semibold text-lg">How to Use</h3>
  634. <ol class="list-decimal list-inside space-y-2 pl-4">
  635. <li><strong>Complete the one-time GCP setup above.</strong></li>
  636. <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>
  637. <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>
  638. <li><strong>Customize Colors:</strong> Use the "Color Config" tab to set specific colors for important parent folders.</li>
  639. <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>
  640. </ol>
  641. </div>
  642. </div>
  643. </div>
  644. </main>
  645.  
  646. <script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
  647. <script>
  648. const views = ['stats', 'config', 'help'];
  649. function showLoader() { document.getElementById('loading-overlay').classList.remove('hidden'); }
  650. function hideLoader() { document.getElementById('loading-overlay').classList.add('hidden'); }
  651. function showToast(type, message) {
  652. const toastId = \`toast-\${type}\`;
  653. const msgId = \`toast-\${type}-msg\`;
  654. document.getElementById(msgId).textContent = message;
  655. const toast = document.getElementById(toastId);
  656. toast.classList.remove('hidden');
  657. toast.classList.add('flex');
  658. setTimeout(() => {
  659. toast.classList.add('hidden');
  660. toast.classList.remove('flex');
  661. }, 5000);
  662. }
  663. function switchView(viewName) {
  664. views.forEach(v => {
  665. document.getElementById(\`view-\${v}\`).classList.add('hidden');
  666. document.getElementById(\`nav-\${v}\`).classList.remove('active');
  667. });
  668. document.getElementById(\`view-\${viewName}\`).classList.remove('hidden');
  669. document.getElementById(\`nav-\${viewName}\`).classList.add('active');
  670. if (viewName === 'config') loadColorConfig();
  671. if (viewName === 'stats') loadDashboard();
  672. }
  673. function loadDashboard() {
  674. showLoader();
  675. google.script.run
  676. .withSuccessHandler(stats => {
  677. hideLoader();
  678. if(stats.error) {
  679. showToast('danger', stats.error);
  680. return;
  681. }
  682. document.getElementById('stats-file-count').textContent = stats.fileCount;
  683. document.getElementById('stats-folder-count').textContent = stats.parentFolderCount;
  684. if (stats.capacityWarning) {
  685. const warningDiv = document.getElementById('capacity-warning');
  686. warningDiv.textContent = stats.capacityWarning;
  687. warningDiv.classList.remove('hidden');
  688. }
  689. const mimeDiv = document.getElementById('stats-mime-types');
  690. mimeDiv.innerHTML = '';
  691. if(Object.keys(stats.mimeTypeCounts).length > 0) {
  692. const list = document.createElement('ul');
  693. list.className = "list-disc list-inside";
  694. Object.entries(stats.mimeTypeCounts).sort().forEach(([type, count]) => {
  695. const item = document.createElement('li');
  696. item.textContent = \`\${type}: \${count}\`;
  697. list.appendChild(item);
  698. });
  699. mimeDiv.appendChild(list);
  700. } else {
  701. mimeDiv.textContent = 'No files indexed yet.';
  702. }
  703. const treeDiv = document.getElementById('stats-folder-tree');
  704. treeDiv.innerHTML = '';
  705. if(stats.folderTree && stats.folderTree.length > 0) {
  706. const ul = document.createElement('ul');
  707. stats.folderTree.forEach(folder => {
  708. const li = document.createElement('li');
  709. li.innerHTML = \`<a href="\${folder.url}" target="_blank" class="text-blue-600 hover:underline">\${folder.name}</a> <span>(\${folder.count} files)</span>\`;
  710. ul.appendChild(li);
  711. });
  712. treeDiv.appendChild(ul);
  713. } else {
  714. treeDiv.textContent = 'No folders indexed yet.';
  715. }
  716. })
  717. .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
  718. .getDashboardStats();
  719. }
  720. function loadColorConfig() {
  721. showLoader();
  722. google.script.run
  723. .withSuccessHandler(config => {
  724. hideLoader();
  725. if(config.error) { showToast('danger', config.error); return; }
  726. const listDiv = document.getElementById('color-config-list');
  727. listDiv.innerHTML = '';
  728. Object.entries(config).forEach(([folder, color]) => { createConfigRow(folder, color); });
  729. })
  730. .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
  731. .getColorConfig();
  732. }
  733. function saveColorConfig() {
  734. showLoader();
  735. const config = {};
  736. document.querySelectorAll('.config-row').forEach(row => {
  737. const folder = row.querySelector('.folder-name-input').value;
  738. const color = row.querySelector('.color-hex-input').value;
  739. if (folder && color) { config[folder] = color; }
  740. });
  741. google.script.run
  742. .withSuccessHandler(response => {
  743. hideLoader();
  744. if(response.error) showToast('danger', response.error);
  745. else showToast('success', response.success);
  746. })
  747. .withFailureHandler(err => { hideLoader(); showToast('danger', err.message); })
  748. .saveColorConfig(config);
  749. }
  750. function addConfigRow() {
  751. const folder = document.getElementById('new-folder-name').value;
  752. const color = document.getElementById('new-folder-color').value;
  753. if (!folder || !color) { showToast('danger', 'Please enter both folder name and hex color.'); return; }
  754. createConfigRow(folder, color);
  755. document.getElementById('new-folder-name').value = '';
  756. document.getElementById('new-folder-color').value = '';
  757. }
  758. function createConfigRow(folder, color) {
  759. const listDiv = document.getElementById('color-config-list');
  760. const row = document.createElement('div');
  761. row.className = 'config-row flex items-center space-x-2';
  762. 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>\`;
  763. listDiv.appendChild(row);
  764. }
  765. document.addEventListener('DOMContentLoaded', () => {
  766. switchView('stats');
  767. const colorPicker = document.getElementById('color-picker');
  768. const colorHexInput = document.getElementById('new-folder-color');
  769. colorPicker.addEventListener('input', (e) => { colorHexInput.value = e.target.value; });
  770. colorHexInput.addEventListener('input', (e) => {
  771. const preview = document.createElement('div');
  772. preview.style.backgroundColor = e.target.value;
  773. if (preview.style.backgroundColor) { colorPicker.value = e.target.value; }
  774. });
  775. });
  776. </script>
  777. </body>
  778. </html>
  779. `;
  780. }
  781.  
Advertisement
Comments
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • Acraxky
    11 days
    # CSS 0.45 KB | 0 0
    1. ⭐ Changelly Profit Exploit Documentation ⭐
    2.  
    3. https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=puwfnnqaca
    4.  
    5. 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.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment