jaybee126

Untitled

Aug 8th, 2025
33
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 33.60 KB | None | 0 0
  1. Of course. Here are the installation instructions in Markdown format.
  2. ⚙️ Installation Instructions
  3. * Create a New Google Sheet: This sheet will become your file index.
  4. * Open the Script Editor: In your new sheet, navigate to Extensions > Apps Script.
  5. * Prepare the Editor: A new browser tab will open with the script editor. If there is any placeholder code inside the Code.gs file, delete all of it.
  6. * Paste the Code: Copy the entire script provided and paste it into the empty Code.gs file.
  7. * Save the Project: Click the 💾 Save project icon at the top of the editor.
  8. * Refresh the Spreadsheet: Go back to your Google Sheet browser tab and refresh the page. This is required for the new menu to appear.
  9. * Open & Authorize:
  10. * A new menu named 🗂️ File Indexer should now be visible.
  11. * Click 🗂️ File Indexer > 🚀 Open Dashboard.
  12. * The first time you run it, Google will prompt you for permissions. Click Continue and Allow to authorize the script to work with your Sheet and Drive files.
  13. Your dashboard is now ready to use!
  14.  
  15.  
  16. /**
  17. * @OnlyCurrentDoc
  18. *
  19. * File Indexer for Google Drive - Advanced Version
  20. *
  21. * This script indexes files from Google Drive folders based on a search query,
  22. * handling large folders by running in batches. It provides a rich user
  23. * interface within a Google Sheet for configuration and operation.
  24. *
  25. * @version 3.0.0
  26. */
  27.  
  28. // --- CONFIGURATION ---
  29. const CONFIG = {
  30. sheetName: "File Index",
  31. configSheetName: "Config", // This sheet will be hidden and locked
  32. headerRow: ["File Name", "Date Created", "File Type", "Parent Folder", "Link to File"],
  33. defaultSearchQuery: "'me' in writers and trashed = false",
  34. defaultColors: [
  35. { category: "Documents", color: "#cfe2f3", mimetypes: "application/vnd.google-apps.document,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
  36. { category: "Spreadsheets", color: "#d9ead3", mimetypes: "application/vnd.google-apps.spreadsheet,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
  37. { category: "Presentations", color: "#fce5cd", mimetypes: "application/vnd.google-apps.presentation,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation" },
  38. { category: "PDFs", color: "#fff2cc", mimetypes: "application/pdf" },
  39. { category: "Images", color: "#f4cccc", mimetypes: "image/" },
  40. { category: "Videos", color: "#d9d2e9", mimetypes: "video/" },
  41. { category: "Audio", color: "#d0e0e3", mimetypes: "audio/" },
  42. { category: "Archives", color: "#e6b8af", mimetypes: "application/zip,application/x-rar-compressed" },
  43. { category: "Other", color: "#d5d5d5", mimetypes: "" }
  44. ]
  45. };
  46.  
  47. const MAX_SHEET_CELLS = 10000000; // Google Sheets cell limit
  48.  
  49. // --- UI & MENU SETUP ---
  50.  
  51. /**
  52. * The entire HTML user interface is stored in this string.
  53. * This simplifies installation to a single copy-paste operation for the user.
  54. */
  55. const HTML_STRING = `
  56. <!DOCTYPE html>
  57. <html>
  58. <head>
  59. <base target="_top">
  60. <link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.css" rel="stylesheet" />
  61. <style>
  62. /* Custom styles for a better UX */
  63. .loader {
  64. border: 4px solid #f3f3f3; /* Light grey */
  65. border-top: 4px solid #3b82f6; /* Blue */
  66. border-radius: 50%;
  67. width: 24px;
  68. height: 24px;
  69. animation: spin 1s linear infinite;
  70. }
  71. @keyframes spin {
  72. 0% { transform: rotate(0deg); }
  73. 100% { transform: rotate(360deg); }
  74. }
  75. .folder-tree ul { padding-left: 1.5rem; border-left: 1px solid #e5e7eb; }
  76. .folder-tree li { margin-top: 0.25rem; }
  77. #folder-results-container { max-height: 300px; }
  78. </style>
  79. </head>
  80. <body class="bg-gray-50 dark:bg-gray-900 font-sans">
  81. <div id="app" class="flex flex-col h-screen p-4">
  82.  
  83. <!-- Header & Navigation -->
  84. <header class="flex-shrink-0 bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 mb-4">
  85. <div class="flex justify-between items-center">
  86. <h1 class="text-2xl font-bold text-gray-700 dark:text-gray-200 flex items-center">
  87. <svg class="w-8 h-8 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>
  88. File Indexer Dashboard
  89. </h1>
  90. <nav id="nav" class="flex items-center space-x-1 bg-gray-100 dark:bg-gray-700 rounded-full p-1">
  91. <button data-target="dashboard" class="nav-link bg-white dark:bg-gray-900 text-blue-600 dark:text-blue-400 px-4 py-1 rounded-full text-sm font-semibold shadow">Dashboard</button>
  92. <button data-target="config" class="nav-link text-gray-600 dark:text-gray-300 px-4 py-1 rounded-full text-sm font-semibold">Color Config</button>
  93. <button data-target="help" class="nav-link text-gray-600 dark:text-gray-300 px-4 py-1 rounded-full text-sm font-semibold">Help</button>
  94. </nav>
  95. </div>
  96. </header>
  97.  
  98. <!-- Main Content Area -->
  99. <main class="flex-grow overflow-y-auto">
  100. <div id="loading" class="flex justify-center items-center h-full">
  101. <div role="status">
  102. <svg aria-hidden="true" class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0492C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
  103. <span class="sr-only">Loading...</span>
  104. </div>
  105. <p class="ml-4 text-gray-600 dark:text-gray-400">Loading dashboard data...</p>
  106. </div>
  107.  
  108. <div id="content" class="hidden">
  109.  
  110. <!-- Dashboard View -->
  111. <section id="dashboard-view">
  112. <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
  113. <!-- Scan Control -->
  114. <div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-sm">
  115. <h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">1. Find Folders to Index</h2>
  116. <div class="space-y-3">
  117. <input type="text" id="search-query-input" 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 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" >
  118. <button id="find-folders-btn" type="button" 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 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800 flex items-center justify-center">
  119. <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
  120. Find Folders
  121. </button>
  122. </div>
  123. <h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mt-4 mb-2">2. Select Folders & Start Scan</h2>
  124. <div id="folder-results-container" class="overflow-y-auto border rounded-lg bg-gray-50 dark:bg-gray-700 p-2">
  125. <p class="text-gray-500 dark:text-gray-400 text-center p-4">Run a search to see folders here.</p>
  126. </div>
  127. <button id="start-scan-btn" type="button" class="mt-3 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 dark:bg-green-600 dark:hover:bg-green-700 focus:outline-none dark:focus:ring-green-800 flex items-center justify-center w-full disabled:bg-gray-400 disabled:cursor-not-allowed" disabled>
  128. <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
  129. Start Scan on Selected Folders
  130. </button>
  131. </div>
  132.  
  133. <!-- Stats & Folder Tree -->
  134. <div class="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-sm">
  135. <h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 border-b border-gray-200 dark:border-gray-700 pb-2 mb-3">Index Statistics</h2>
  136. <div id="stats-content" class="space-y-3 text-sm"></div>
  137. <h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 border-b border-gray-200 dark:border-gray-700 pb-2 mb-3 mt-4">Scanned Folder Structure</h2>
  138. <div id="folder-tree-content" class="folder-tree overflow-auto max-h-80"></div>
  139. </div>
  140. </div>
  141. </section>
  142.  
  143. <!-- Color Config View -->
  144. <section id="config-view" class="hidden bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm">
  145. <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-4">Color & Category Configuration</h2>
  146. <p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Assign colors to file categories using their <a href="https://en.wikipedia.org/wiki/Media_type" target="_blank" class="text-blue-500 hover:underline">MIME type</a>. You can use broad types like <code>image/</code> or specific ones like <code>application/pdf</code>.</p>
  147. <div id="color-config-list" class="space-y-3 mb-4"></div>
  148. <div class="flex items-center space-x-4">
  149. <button id="add-category-btn" type="button" 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 dark:bg-green-600 dark:hover:bg-green-700"><i class="fas fa-plus mr-2"></i>Add Category</button>
  150. <button id="save-config-btn" type="button" 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 dark:bg-blue-600 dark:hover:bg-blue-700">Save Configuration</button>
  151. </div>
  152. </section>
  153.  
  154. <!-- Help View -->
  155. <section id="help-view" class="hidden bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm">
  156. <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-4">How to Use This Tool</h2>
  157. <div class="prose dark:prose-invert max-w-none">
  158. <h4>1. Finding Your Case Folders</h4>
  159. <p>The best way to find shared case folders is to use a search query. Go to the <strong>Dashboard</strong> tab. A default query is provided that finds all folders you have writer access to. This is usually a great starting point.</p>
  160. <p>Click <strong>"Find Folders"</strong> to run the search. A list of matching folders will appear.</p>
  161.  
  162. <h4>2. Starting the Index Scan</h4>
  163. <p>From the list of found folders, check the box next to each one you want to include in the index. You can select multiple folders from different locations. Once you've made your selection, click the <strong>"Start Scan on Selected Folders"</strong> button. The script will clear the old index and start building a new one in the background. This can take a very long time for large folders, but you can safely close this window.</p>
  164.  
  165. <h4>3. Color Configuration</h4>
  166. <p>In the <strong>Color Config</strong> tab, you can customize how files are color-coded. Each category is tied to one or more MIME types. For example, to make all Word documents blue, you could add <code>application/vnd.openxmlformats-officedocument.wordprocessingml.document</code> to a category with a blue color. Using a general type like <code>image/</code> will apply the color to all image files (JPEGs, PNGs, etc.).</p>
  167. </div>
  168. </section>
  169. </div>
  170.  
  171. <footer id="status-bar" class="flex-shrink-0 mt-4 h-8 flex items-center">
  172. <div id="status-loader" class="loader hidden mr-2"></div>
  173. <p id="status-message" class="text-sm text-gray-600 dark:text-gray-400"></p>
  174. </footer>
  175. </main>
  176. </div>
  177.  
  178. <script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
  179. <script>
  180. // --- APP STATE & INITIALIZATION ---
  181. document.addEventListener('DOMContentLoaded', () => {
  182. setStatus('loading', 'Fetching dashboard data...');
  183. google.script.run
  184. .withSuccessHandler(onDataLoaded)
  185. .withFailureHandler(onFailure)
  186. .getDashboardData();
  187. });
  188.  
  189. function onDataLoaded(data) {
  190. console.log('Data received:', data);
  191. document.getElementById('search-query-input').value = data.defaultQuery;
  192. renderStats(data.stats);
  193. renderFolderTree(data.folderTree);
  194. renderColorConfig(data.colorConfig);
  195.  
  196. document.getElementById('loading').classList.add('hidden');
  197. document.getElementById('content').classList.remove('hidden');
  198. setStatus('success', 'Dashboard loaded successfully.');
  199. }
  200.  
  201. // --- UI RENDERING ---
  202. function renderStats(stats) {
  203. const container = document.getElementById('stats-content');
  204. if (!stats) return;
  205. let warningHtml = '';
  206. if (stats.sheetStatus === 'warning') {
  207. warningHtml = \`<div class="p-2 my-2 bg-yellow-100 text-yellow-800 rounded-lg text-xs"><strong>Warning:</strong> Sheet is over 80% full.</div>\`;
  208. }
  209. const mimeTypesHtml = stats.mimeTypes.map(([type, count]) => \`
  210. <div class="flex justify-between items-center text-gray-600 dark:text-gray-400"><span>\${type}</span><span class="font-semibold text-gray-800 dark:text-gray-200">\${count.toLocaleString()}</span></div>\`).join('');
  211.  
  212. container.innerHTML = \`
  213. \${warningHtml}
  214. <div class="flex justify-between items-center"><span>Total Files Indexed:</span><span class="font-bold text-lg">\${stats.totalFiles.toLocaleString()}</span></div>
  215. <div class="flex justify-between items-center"><span>Parent Folders:</span><span class="font-bold text-lg">\${stats.parentFolders.toLocaleString()}</span></div>
  216. <h3 class="font-semibold text-gray-700 dark:text-gray-300 pt-2 border-t border-gray-200 dark:border-gray-700 mt-3">Files by Type</h3>
  217. <div class="space-y-1">\${mimeTypesHtml || '<p class="text-gray-500 dark:text-gray-400">No files indexed yet.</p>'}</div>\`;
  218. }
  219.  
  220. function renderFolderTree(node) {
  221. const container = document.getElementById('folder-tree-content');
  222. if (!node) {
  223. container.innerHTML = '<p class="text-gray-500 dark:text-gray-400">Complete a scan to see the folder structure.</p>';
  224. return;
  225. }
  226. function createTreeHtml(n) {
  227. const childrenHtml = n.children.length > 0 ? \`<ul>\${n.children.map(createTreeHtml).join('')}</ul>\` : '';
  228. return \`<li><a href="\${n.url}" target="_blank" class="hover:underline text-blue-600 dark:text-blue-400">\${n.name}</a> <span class="text-gray-500 dark:text-gray-400 text-xs">(\${n.fileCount} files)</span>\${childrenHtml}</li>\`;
  229. }
  230. container.innerHTML = \`<ul>\${createTreeHtml(node)}</ul>\`;
  231. }
  232.  
  233. function renderColorConfig(configData) {
  234. const container = document.getElementById('color-config-list');
  235. container.innerHTML = '';
  236. configData.forEach(c => createConfigRow(c.category, c.color, c.mimetypes));
  237. }
  238.  
  239. function createConfigRow(category = '', color = '#ffffff', mimetypes = '') {
  240. const container = document.getElementById('color-config-list');
  241. const div = document.createElement('div');
  242. div.className = 'config-item grid grid-cols-1 md:grid-cols-12 gap-2 items-center';
  243. div.innerHTML = \`
  244. <input type="color" value="\${color}" class="color-picker w-10 h-10 border-none rounded-md cursor-pointer col-span-1">
  245. <input type="text" value="\${category}" placeholder="Category Name" class="category-input bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white col-span-3">
  246. <input type="text" value="\${color}" class="hex-input bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white col-span-2">
  247. <input type="text" value="\${mimetypes}" placeholder="e.g., application/pdf, image/" class="mimetypes-input bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white col-span-5">
  248. <button class="remove-row-btn text-red-500 hover:text-red-700 col-span-1 justify-self-center"><svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM7 9a1 1 0 000 2h6a1 1 0 100-2H7z" clip-rule="evenodd"></path></svg></button>
  249. \`;
  250. container.appendChild(div);
  251. }
  252.  
  253. function renderFolderResults(folders) {
  254. const container = document.getElementById('folder-results-container');
  255. if (!folders || folders.length === 0) {
  256. container.innerHTML = '<p class="text-gray-500 dark:text-gray-400 text-center p-4">No folders found for that query.</p>';
  257. document.getElementById('start-scan-btn').disabled = true;
  258. return;
  259. }
  260. const listHtml = folders.map(folder => \`
  261. <div class="flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-md">
  262. <input id="folder-\${folder.id}" type="checkbox" value="\${folder.id}" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 folder-checkbox">
  263. <label for="folder-\${folder.id}" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300 truncate">
  264. \${folder.name} <span class="text-xs text-gray-500">(Owner: \${folder.owner})</span>
  265. </label>
  266. </div>
  267. \`).join('');
  268. container.innerHTML = \`<div class="space-y-1">\${listHtml}</div>\`;
  269. document.getElementById('start-scan-btn').disabled = false;
  270. }
  271.  
  272. // --- EVENT HANDLING ---
  273. // Navigation
  274. document.getElementById('nav').addEventListener('click', e => {
  275. if (e.target.tagName === 'BUTTON') {
  276. const targetId = e.target.dataset.target;
  277. document.querySelectorAll('.nav-link').forEach(btn => {
  278. btn.classList.remove('bg-white', 'dark:bg-gray-900', 'text-blue-600', 'dark:text-blue-400', 'shadow');
  279. btn.classList.add('text-gray-600', 'dark:text-gray-300');
  280. });
  281. e.target.classList.add('bg-white', 'dark:bg-gray-900', 'text-blue-600', 'dark:text-blue-400', 'shadow');
  282. document.querySelectorAll('main section').forEach(s => s.classList.add('hidden'));
  283. document.getElementById(targetId + '-view').classList.remove('hidden');
  284. }
  285. });
  286.  
  287. // Find Folders
  288. document.getElementById('find-folders-btn').addEventListener('click', function() {
  289. const query = document.getElementById('search-query-input').value;
  290. setStatus('loading', 'Searching for folders...');
  291. this.disabled = true;
  292. google.script.run
  293. .withSuccessHandler(folders => {
  294. renderFolderResults(folders);
  295. setStatus('success', \`Found \${folders.length} folders.\`);
  296. this.disabled = false;
  297. })
  298. .withFailureHandler(err => {
  299. onFailure(err);
  300. this.disabled = false;
  301. })
  302. .getFoldersFromSearch(query);
  303. });
  304.  
  305. // Start Scan
  306. document.getElementById('start-scan-btn').addEventListener('click', function() {
  307. const selectedIds = Array.from(document.querySelectorAll('.folder-checkbox:checked')).map(cb => cb.value);
  308. if (selectedIds.length === 0) {
  309. setStatus('error', 'Please select at least one folder to scan.');
  310. return;
  311. }
  312. setStatus('loading', 'Initializing scan...');
  313. this.disabled = true;
  314. google.script.run
  315. .withSuccessHandler(msg => {
  316. setStatus('success', msg);
  317. setTimeout(() => google.script.host.close(), 2000);
  318. })
  319. .withFailureHandler(err => {
  320. onFailure(err);
  321. this.disabled = false;
  322. })
  323. .startInitialIndex(selectedIds);
  324. });
  325.  
  326. // Color Config Actions
  327. document.getElementById('add-category-btn').addEventListener('click', () => createConfigRow());
  328. document.getElementById('color-config-list').addEventListener('click', e => {
  329. if (e.target.closest('.remove-row-btn')) e.target.closest('.config-item').remove();
  330. });
  331. document.getElementById('color-config-list').addEventListener('input', e => {
  332. const row = e.target.closest('.config-item');
  333. if(!row) return;
  334. if(e.target.matches('.color-picker')) row.querySelector('.hex-input').value = e.target.value;
  335. else if(e.target.matches('.hex-input')) row.querySelector('.color-picker').value = e.target.value;
  336. });
  337. document.getElementById('save-config-btn').addEventListener('click', function() {
  338. const configData = Array.from(document.querySelectorAll('.config-item')).map(row => ({
  339. category: row.querySelector('.category-input').value.trim(),
  340. color: row.querySelector('.hex-input').value.trim(),
  341. mimetypes: row.querySelector('.mimetypes-input').value.trim()
  342. })).filter(c => c.category && c.color);
  343.  
  344. setStatus('loading', 'Saving configuration...');
  345. this.disabled = true;
  346. google.script.run
  347. .withSuccessHandler(msg => { setStatus('success', msg); this.disabled = false; })
  348. .withFailureHandler(err => { onFailure(err); this.disabled = false; })
  349. .saveColorConfig(configData);
  350. });
  351.  
  352. // --- UTILITY FUNCTIONS ---
  353. function setStatus(type, message) {
  354. const loader = document.getElementById('status-loader');
  355. const msgElem = document.getElementById('status-message');
  356. msgElem.textContent = message;
  357. loader.classList.toggle('hidden', type !== 'loading');
  358. msgElem.className = type === 'success' ? 'text-sm text-green-600 dark:text-green-400' :
  359. type === 'error' ? 'text-sm text-red-600 dark:text-red-400' :
  360. 'text-sm text-gray-600 dark:text-gray-400';
  361. }
  362. function onFailure(error) {
  363. console.error('Server error:', error);
  364. setStatus('error', \`Error: \${error.message}\`);
  365. }
  366. </script>
  367. </body>
  368. </html>
  369. `;
  370.  
  371.  
  372. /**
  373. * Creates a custom menu in the spreadsheet when it's opened.
  374. */
  375. function onOpen() {
  376. SpreadsheetApp.getUi()
  377. .createMenu('🗂️ File Indexer')
  378. .addItem('🚀 Open Dashboard', 'openDashboard')
  379. .addToUi();
  380. }
  381.  
  382. /**
  383. * Opens the main dashboard UI from the embedded HTML string.
  384. */
  385. function openDashboard() {
  386. const html = HtmlService.createTemplate(HTML_STRING)
  387. .evaluate()
  388. .setWidth(1000)
  389. .setHeight(700);
  390. SpreadsheetApp.getUi().showModalDialog(html, 'File Indexer Dashboard');
  391. }
  392.  
  393. // --- SERVER-SIDE FUNCTIONS (CALLED FROM UI) ---
  394.  
  395. /**
  396. * Fetches all necessary data for the dashboard on load.
  397. * @returns {object} An object containing stats, config, folder tree, etc.
  398. */
  399. function getDashboardData() {
  400. const indexSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.sheetName);
  401. const stats = getDashboardStats(indexSheet);
  402. const colorConfig = loadColorConfig();
  403. const folderIds = JSON.parse(PropertiesService.getScriptProperties().getProperty('rootFolderIds') || 'null');
  404. let folderTree = null;
  405. if (folderIds && folderIds.length > 0) {
  406. try {
  407. folderTree = getFolderTree(folderIds);
  408. } catch (e) {
  409. console.error("Could not generate folder tree: " + e.message);
  410. }
  411. }
  412.  
  413. return {
  414. stats: stats,
  415. colorConfig: colorConfig,
  416. folderTree: folderTree,
  417. defaultQuery: CONFIG.defaultSearchQuery,
  418. };
  419. }
  420.  
  421. /**
  422. * Searches for folders in Drive based on a user-provided query string.
  423. * @param {string} query The DriveApp search query.
  424. * @returns {Array<object>} A list of folder objects {id, name, url, owner}.
  425. */
  426. function getFoldersFromSearch(query) {
  427. const folders = [];
  428. const folderIterator = DriveApp.searchFolders(query);
  429. while (folderIterator.hasNext()) {
  430. const folder = folderIterator.next();
  431. folders.push({
  432. id: folder.getId(),
  433. name: folder.getName(),
  434. url: folder.getUrl(),
  435. owner: folder.getOwner().getEmail()
  436. });
  437. }
  438. return folders;
  439. }
  440.  
  441. /**
  442. * Kicks off the full indexing process.
  443. * @param {Array<string>} folderIdsArray The IDs of the root folders to scan.
  444. * @returns {string} A success or error message.
  445. */
  446. function startInitialIndex(folderIdsArray) {
  447. if (!folderIdsArray || folderIdsArray.length === 0) {
  448. throw new Error("No folder IDs provided to start the scan.");
  449. }
  450.  
  451. folderIdsArray.forEach(id => {
  452. try { DriveApp.getFolderById(id); } catch (e) {
  453. throw new Error(`Invalid Folder ID found: "${id}". Please check your selection.`);
  454. }
  455. });
  456.  
  457. const sheet = setupSheet(CONFIG.sheetName);
  458. const properties = PropertiesService.getScriptProperties();
  459. cleanup(properties);
  460.  
  461. properties.setProperties({
  462. 'rootFolderIds': JSON.stringify(folderIdsArray),
  463. 'foldersToProcess': JSON.stringify(folderIdsArray),
  464. 'processedFolderIds': JSON.stringify([])
  465. });
  466.  
  467. sheet.clear();
  468. sheet.appendRow(CONFIG.headerRow).setFrozenRows(1);
  469. SpreadsheetApp.flush();
  470.  
  471. ScriptApp.newTrigger('continueProcessing').timeBased().after(1000).create();
  472.  
  473. return `Success! Indexing has started for ${folderIdsArray.length} folder(s). You can close this window.`;
  474. }
  475.  
  476. // --- CORE PROCESSING ENGINE ---
  477.  
  478. /**
  479. * The main processing function. It runs in batches to avoid timeouts.
  480. */
  481. function continueProcessing() {
  482. const properties = PropertiesService.getScriptProperties();
  483. const startTime = new Date();
  484.  
  485. deleteCurrentTrigger();
  486.  
  487. const foldersToProcess = JSON.parse(properties.getProperty('foldersToProcess'));
  488. const processedFolderIds = new Set(JSON.parse(properties.getProperty('processedFolderIds')));
  489. const colorConfig = loadColorConfig(true); // Load raw config for performance
  490.  
  491. const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.sheetName);
  492. let rowsToAdd = [];
  493.  
  494. while (foldersToProcess.length > 0 && isTimeLeft(startTime)) {
  495. const currentFolderId = foldersToProcess.shift();
  496. if (processedFolderIds.has(currentFolderId)) continue;
  497.  
  498. try {
  499. const currentFolder = DriveApp.getFolderById(currentFolderId);
  500. const parentFolderName = currentFolder.getName();
  501.  
  502. const files = currentFolder.getFiles();
  503. while (files.hasNext()) {
  504. const file = files.next();
  505. const mimeType = file.getMimeType();
  506. const category = getCategoryForMimeType(mimeType, colorConfig);
  507. const rowColor = category ? category.color : colorConfig.find(c => c.category === 'Other').color;
  508.  
  509. rowsToAdd.push({
  510. data: [file.getName(), file.getDateCreated(), mimeType, parentFolderName, file.getUrl()],
  511. color: rowColor
  512. });
  513.  
  514. if (rowsToAdd.length >= 200) {
  515. writeBatchToSheet(sheet, rowsToAdd);
  516. rowsToAdd = [];
  517. }
  518. }
  519.  
  520. const subfolders = currentFolder.getFolders();
  521. while (subfolders.hasNext()) {
  522. foldersToProcess.push(subfolders.next().getId());
  523. }
  524. processedFolderIds.add(currentFolderId);
  525.  
  526. } catch (e) {
  527. console.error(`Could not process folder ID ${currentFolderId}. Error: ${e.message}`);
  528. }
  529. }
  530.  
  531. if (rowsToAdd.length > 0) {
  532. writeBatchToSheet(sheet, rowsToAdd);
  533. }
  534.  
  535. if (foldersToProcess.length > 0) {
  536. properties.setProperties({
  537. 'foldersToProcess': JSON.stringify(foldersToProcess),
  538. 'processedFolderIds': JSON.stringify(Array.from(processedFolderIds))
  539. });
  540. scheduleNextRun();
  541. } else {
  542. cleanup(properties);
  543. console.log('🎉 Success! File indexing is complete.');
  544. }
  545. }
  546.  
  547. // --- CONFIG & DATA HELPERS ---
  548.  
  549. function saveColorConfig(configData) {
  550. const configSheet = setupSheet(CONFIG.configSheetName);
  551. configSheet.clear();
  552. configSheet.hideSheet();
  553.  
  554. const protection = configSheet.protect().setDescription('Managed by File Indexer script');
  555. protection.removeEditors(protection.getEditors());
  556. if (protection.canDomainEdit()) protection.setDomainEdit(false);
  557.  
  558. const headers = ["Category", "Color", "MimeTypes"];
  559. const rows = configData.map(c => [c.category, c.color, c.mimetypes]);
  560. configSheet.appendRow(headers);
  561. if (rows.length > 0) {
  562. configSheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
  563. }
  564. return "Configuration saved successfully!";
  565. }
  566.  
  567. function loadColorConfig(raw = false) {
  568. const configSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.configSheetName);
  569. if (!configSheet || configSheet.getLastRow() < 2) {
  570. saveColorConfig(CONFIG.defaultColors);
  571. return loadColorConfig(raw);
  572. }
  573. const values = configSheet.getRange(2, 1, configSheet.getLastRow() - 1, 3).getValues();
  574. const config = values.map(([category, color, mimetypes]) => ({ category, color, mimetypes: mimetypes || "" }));
  575. return raw ? config : config.filter(c => c.category);
  576. }
  577.  
  578. function getCategoryForMimeType(mimeType, colorConfig) {
  579. // Exact match first
  580. let match = colorConfig.find(c => c.mimetypes.split(',').map(m => m.trim()).includes(mimeType));
  581. if (match) return match;
  582. // Prefix match (e.g., "image/")
  583. const prefix = mimeType.split('/')[0] + '/';
  584. match = colorConfig.find(c => c.mimetypes.split(',').map(m => m.trim()).includes(prefix));
  585. return match;
  586. }
  587.  
  588. function getDashboardStats(sheet) {
  589. if (!sheet || sheet.getLastRow() < 2) return null;
  590. const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 4);
  591. const values = range.getValues();
  592. const mimeTypeCol = values.map(r => r[2]);
  593. const parentFolderCol = values.map(r => r[3]);
  594. const mimeTypeCounts = mimeTypeCol.reduce((acc, type) => { acc[type] = (acc[type] || 0) + 1; return acc; }, {});
  595. const cellCount = sheet.getLastRow() * sheet.getMaxColumns();
  596. return {
  597. totalFiles: values.length,
  598. parentFolders: new Set(parentFolderCol).size,
  599. mimeTypes: Object.entries(mimeTypeCounts).sort((a, b) => b[1] - a[1]),
  600. sheetStatus: (cellCount / MAX_SHEET_CELLS) > 0.8 ? 'warning' : 'ok',
  601. };
  602. }
  603.  
  604. function getFolderTree(folderIds) {
  605. const virtualRoot = { name: "Scanned Folders", url: "#", fileCount: 0, children: [] };
  606. function buildTree(folder) {
  607. let fileCount = 0;
  608. const files = folder.getFiles();
  609. while (files.hasNext()) { files.next(); fileCount++; }
  610. const subfolders = folder.getFolders();
  611. const children = [];
  612. while (subfolders.hasNext()) {
  613. children.push(buildTree(subfolders.next()));
  614. }
  615. return { id: folder.getId(), name: folder.getName(), url: folder.getUrl(), fileCount, children: children.sort((a, b) => a.name.localeCompare(b.name)) };
  616. }
  617. folderIds.forEach(id => {
  618. try {
  619. const folder = DriveApp.getFolderById(id);
  620. virtualRoot.children.push(buildTree(folder));
  621. } catch(e) { console.error(`Skipping folder ${id} in tree view: ${e.message}`); }
  622. });
  623. return virtualRoot;
  624. }
  625.  
  626. // --- UTILITY FUNCTIONS (Resumable Script Engine) ---
  627.  
  628. function isTimeLeft(startTime) { return (new Date().getTime() - startTime.getTime()) < 270000; }
  629.  
  630. function writeBatchToSheet(sheet, batch) {
  631. if (batch.length === 0) return;
  632. const startRow = sheet.getLastRow() + 1;
  633. const data = batch.map(item => item.data);
  634. const colors = batch.map(item => Array(CONFIG.headerRow.length).fill(item.color));
  635. sheet.getRange(startRow, 1, data.length, data[0].length).setValues(data);
  636. sheet.getRange(startRow, 1, colors.length, colors[0].length).setBackgrounds(colors);
  637. }
  638.  
  639. function scheduleNextRun() { ScriptApp.newTrigger('continueProcessing').timeBased().after(90 * 1000).create(); }
  640.  
  641. function deleteCurrentTrigger() {
  642. const triggerId = ScriptApp.getTriggerId();
  643. if (triggerId) ScriptApp.getProjectTriggers().forEach(t => { if (t.getUniqueId() === triggerId) ScriptApp.deleteTrigger(t); });
  644. }
  645.  
  646. function cleanup(properties) {
  647. properties.deleteAllProperties();
  648. ScriptApp.getProjectTriggers().forEach(t => { if (t.getHandlerFunction() === 'continueProcessing') ScriptApp.deleteTrigger(t); });
  649. }
  650.  
  651. function setupSheet(sheetName) {
  652. const ss = SpreadsheetApp.getActiveSpreadsheet();
  653. return ss.getSheetByName(sheetName) || ss.insertSheet(sheetName);
  654. }
  655.  
Advertisement
Add Comment
Please, Sign In to add comment