πŸ“ Google Drive File Manager

A self-hosted web app that lets you upload files to a folder in your own Google Drive, manage them, get raw download links, merge local file contents into copy blocks, and bulk-change sharing permissions.

πŸ”’ Privacy & Security: When deployed correctly with Execute as: User accessing the web app, each person uses their own Google account. Their files go into their own Drive, not the template owner's Drive.
⚠️ Critical deployment setting: This app must be deployed with Execute as: User accessing the web app. If someone accidentally deploys it as Execute as: Me, Drive actions can run under the deployer's Google account instead of each user's account. That can let other people upload files into the deployer's Drive folder, view/manage files listed by the app, move selected files to Trash, or change sharing settings for files the script can access.

βž• Add to Your Google Account

This button starts the safe setup flow. It opens a new Google Apps Script project in your Google account. Google still requires you to paste the three files below and manually deploy the web app so you can confirm Execute as: User accessing the web app.

βž• Create Apps Script Project πŸ“‹ Jump to Copy Buttons

A truly automatic β€œcreate project + add files + deploy web app” installer requires a separate OAuth app using the Apps Script API. This simple page avoids asking users for broad project-management permissions.

πŸš€ How It Works

  1. Upload – Choose multiple files. A popup asks if you want to make them public. Files go into a folder called Web Uploads in your Google Drive, and raw download URLs appear in a box.
  2. Merge Contents – Choose local text/code files, set a max line count per copy block, and the app merges the file contents into copy-ready blocks with filename begin/end markers.
  3. File Manager – Lists all files inside that folder. Click column headers to sort. Use checkboxes to select files.
  4. Bulk Actions with selected files:

πŸ“‹ Complete Code

You need three files in the Apps Script project: Code.gs, index.html, and instructions.html. Each code area below is capped with its own scrollbar so the page does not get huge.

πŸ“„ Code.gs

function doGet(e) {
  // If ?page=instructions, show instructions page; else show file manager
  if (e && e.parameter && e.parameter.page === 'instructions') {
    return HtmlService.createHtmlOutputFromFile('instructions')
      .setTitle('Drive File Manager – Instructions')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
  }
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('Drive File Manager')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// Add this somewhere among your other functions
function getScriptUrl() {
  return ScriptApp.getService().getUrl();
}


// --- The rest of the server-side functions stay exactly the same ---
function getFolder_() {
  const folderName = 'Web Uploads';
  const folders = DriveApp.getFoldersByName(folderName);
  if (folders.hasNext()) {
    return folders.next();
  } else {
    return DriveApp.createFolder(folderName);
  }
}

function processUploads(files, makePublic) {
  const folder = getFolder_();
  const results = [];
  files.forEach(file => {
    const parts = file.data.split(',');
    const mimeType = parts[0].match(/:(.*?);/)[1];
    const blob = Utilities.newBlob(Utilities.base64Decode(parts[1]), mimeType, file.name);
    const driveFile = folder.createFile(blob);
    if (makePublic) {
      driveFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
      results.push(`https://drive.google.com/file/d/${driveFile.getId()}/view?usp=sharing`);
    } else {
      results.push(driveFile.getUrl());
    }
  });
  return results;
}

function getFileList() {
  const folder = getFolder_();
  const files = folder.getFiles();
  const list = [];
  while (files.hasNext()) {
    const file = files.next();
    list.push({
      id: file.getId(),
      name: file.getName(),
      mimeType: file.getMimeType(),
      size: file.getSize(),
      lastUpdated: file.getLastUpdated().toISOString(),
      url: file.getUrl()
    });
  }
  return list;
}

function deleteFiles(ids) {
  ids.forEach(id => {
    try { DriveApp.getFileById(id).setTrashed(true); } catch (e) {}
  });
}

function changeSharing(ids, makePublic) {
  ids.forEach(id => {
    try {
      const file = DriveApp.getFileById(id);
      if (makePublic) {
        file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
      } else {
        file.setSharing(DriveApp.Access.PRIVATE, DriveApp.Permission.NONE);
      }
    } catch (e) {}
  });
}

πŸ“„ index.html

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    body { font-family: Arial, sans-serif; max-width: 900px; margin: 20px auto; padding: 0 15px; }
    button, .btn { padding: 8px 16px; margin: 5px; cursor: pointer; }
    .hidden { display: none; }
    #urlBox, .mergeBox { width: 100%; height: 160px; margin-top: 10px; box-sizing: border-box; font-family: monospace; }
    .mergeBox { height: 260px; }
    .mergeControls { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px; font-size: 14px; }
    .mergeControls input { width: 90px; padding: 7px; }
    .mergeBlock { margin-top: 14px; padding: 12px; background: #fff; border: 1px solid #ddd; border-radius: 5px; }
    .mergeBlockHeader { font-weight: bold; margin-bottom: 4px; }
    .mergeWarning { color: #a15c00; font-weight: bold; }
    table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: left; }
    th { background: #f4f4f4; cursor: pointer; user-select: none; }
    .sort-indicator::after { content: ' ↕'; }
    .sort-asc::after { content: ' ↑'; }
    .sort-desc::after { content: ' ↓'; }
    .progress { margin: 10px 0; font-style: italic; }
    #linkSection, #mergeSection { margin-top: 20px; background: #f9f9f9; padding: 15px; border-radius: 5px; }
  </style>
</head>
<body>
  <h2>πŸ“ Drive File Manager</h2>

  <!-- Upload / Merge Section -->
  <div>
    <input type="file" id="fileInput" multiple class="hidden" accept="*/*" />
    <input type="file" id="mergeFileInput" multiple class="hidden" accept="*/*" />
    <button onclick="document.getElementById('fileInput').click()" class="btn">πŸ“€ Select Files</button>
    <button onclick="document.getElementById('mergeFileInput').click()" class="btn">🧩 Merge Contents</button>
    <span class="mergeControls">
      <label for="mergeMaxLines">Max lines per block:</label>
      <input type="number" id="mergeMaxLines" min="1" value="2000">
    </span>
    <div id="uploadProgress" class="progress"></div>
    <div id="mergeProgress" class="progress"></div>
  </div>

  <!-- Link display section -->
  <div id="linkSection" class="hidden">
    <h4>Uploaded File Links (copy all):</h4>
    <textarea id="urlBox" readonly></textarea>
    <br>
    <button onclick="copyLinks()">πŸ“‹ Copy Links</button>
    <button onclick="document.getElementById('linkSection').classList.add('hidden')">βœ– Close</button>
  </div>

  <!-- Merged contents display section -->
  <div id="mergeSection" class="hidden">
    <h4>Merged File Contents</h4>
    <div class="muted">Each copy block stays under your max line count when possible. If one file is bigger than the max by itself, it will be placed alone and marked with a warning.</div>
    <div id="mergeSummary" class="progress"></div>
    <div id="mergeBlocks"></div>
    <br>
    <button onclick="copyAllMergedBlocks()">πŸ“‹ Copy All Blocks</button>
    <button onclick="document.getElementById('mergeSection').classList.add('hidden')">βœ– Close</button>
  </div>

  <!-- File Manager Table -->
  <h3>πŸ“‹ Files in Folder</h3>
  <button onclick="deleteSelected()" class="btn" style="background:#ff4d4d; color:white;">πŸ—‘ Delete Selected</button>
  <button onclick="copySelectedUrls()" class="btn">πŸ”— Copy URLs of Selected</button>
  <button onclick="changeSharingSelected()" class="btn">πŸ”’ Change Sharing of Selected</button>

  <table id="fileTable">
    <thead>
      <tr>
        <th style="width:30px;"><input type="checkbox" id="selectAll" onclick="toggleAll(this)"></th>
        <th data-sort="name" class="sort-indicator">Name</th>
        <th data-sort="mimeType" class="sort-indicator">Type</th>
        <th data-sort="size" class="sort-indicator">Size</th>
        <th data-sort="lastUpdated" class="sort-indicator">Last Modified</th>
      </tr>
    </thead>
    <tbody id="fileList"></tbody>
  </table>

  <script>
    var filesData = [];          // Full file list from server
    var sortColumn = 'name';
    var sortAsc = true;

    // Fetch and render file list on load
    window.onload = function() {
      refreshFileList();
    };

    // ========== Upload Logic ==========
    var fileInput = document.getElementById('fileInput');
    fileInput.addEventListener('change', handleFileSelect);

    var mergeFileInput = document.getElementById('mergeFileInput');
    mergeFileInput.addEventListener('change', handleMergeFileSelect);

    function handleFileSelect(event) {
      var files = event.target.files;
      if (!files.length) return;

      var makePublic = confirm('Do you want to make all uploaded files publicly viewable?');
      var progressDiv = document.getElementById('uploadProgress');
      progressDiv.textContent = 'Reading files...';

      var readers = [];
      for (var i = 0; i < files.length; i++) {
        readers.push(readFileAsDataURL(files[i]));
      }

      Promise.all(readers)
        .then(function(dataUrls) {
          var uploadPayload = dataUrls.map(function(data, idx) {
            return {
              name: files[idx].name,
              data: data
            };
          });
          progressDiv.textContent = 'Uploading to Drive...';
          return google.script.run.withSuccessHandler(uploadDone).processUploads(uploadPayload, makePublic);
        })
        .catch(function(err) {
          progressDiv.textContent = 'Error: ' + err;
        });
    }

    function readFileAsDataURL(file) {
      return new Promise(function(resolve, reject) {
        var reader = new FileReader();
        reader.onload = function(e) { resolve(e.target.result); };
        reader.onerror = reject;
        reader.readAsDataURL(file);
      });
    }

    function uploadDone(links) {
      document.getElementById('uploadProgress').textContent = '';
      fileInput.value = '';

      if (links.length > 0) {
        var rawLinks = links.map(function(link) {
          return makeRawDriveUrl(link);
        });
        document.getElementById('urlBox').value = rawLinks.join('\n');
        document.getElementById('linkSection').classList.remove('hidden');
      }
      refreshFileList();
    }

    function copyLinks() {
      var urlBox = document.getElementById('urlBox');
      urlBox.select();
      document.execCommand('copy');
      alert('Links copied to clipboard!');
    }

    // ========== Merge Local File Contents Logic ==========
    function handleMergeFileSelect(event) {
      var files = Array.prototype.slice.call(event.target.files || []);
      if (!files.length) return;

      var maxLines = getMergeMaxLines();
      var progressDiv = document.getElementById('mergeProgress');
      progressDiv.textContent = 'Reading files to merge...';

      var readers = files.map(function(file, idx) {
        return readFileAsText(file).then(function(contents) {
          return {
            index: idx + 1,
            name: file.name,
            contents: contents
          };
        });
      });

      Promise.all(readers)
        .then(function(parts) {
          var fileBlocks = parts.map(function(part) {
            var text = [
              'Filename ' + part.index + ':  ' + part.name,
              '< -- begin Filename ' + part.index + ':  ' + part.name + ' -- >',
              part.contents,
              '< -- end Filename ' + part.index + ':  ' + part.name + ' -- >'
            ].join('\n');

            return {
              name: part.name,
              text: text,
              lineCount: countLines(text)
            };
          });

          var blocks = splitIntoCopyBlocks(fileBlocks, maxLines);
          renderMergeBlocks(blocks, maxLines, parts.length);

          document.getElementById('mergeSection').classList.remove('hidden');
          progressDiv.textContent = 'Merged ' + parts.length + ' file(s) into ' + blocks.length + ' copy block(s).';
          mergeFileInput.value = '';
        })
        .catch(function(err) {
          progressDiv.textContent = 'Merge error: ' + err;
        });
    }

    function getMergeMaxLines() {
      var input = document.getElementById('mergeMaxLines');
      var value = parseInt(input.value, 10);

      if (!value || value < 1) {
        value = 2000;
        input.value = value;
      }

      return value;
    }

    function splitIntoCopyBlocks(fileBlocks, maxLines) {
      var copyBlocks = [];
      var currentFiles = [];
      var currentTextParts = [];
      var currentLines = 0;

      fileBlocks.forEach(function(fileBlock) {
        var separatorLines = currentTextParts.length ? 2 : 0;
        var projectedLines = currentLines + separatorLines + fileBlock.lineCount;

        if (currentTextParts.length && projectedLines > maxLines) {
          copyBlocks.push({
            files: currentFiles,
            text: currentTextParts.join('\n\n'),
            lineCount: currentLines,
            overLimit: false
          });

          currentFiles = [];
          currentTextParts = [];
          currentLines = 0;
        }

        currentFiles.push(fileBlock.name);
        currentTextParts.push(fileBlock.text);
        currentLines += (currentTextParts.length > 1 ? 2 : 0) + fileBlock.lineCount;

        if (currentTextParts.length === 1 && fileBlock.lineCount > maxLines) {
          copyBlocks.push({
            files: currentFiles,
            text: currentTextParts.join('\n\n'),
            lineCount: currentLines,
            overLimit: true
          });

          currentFiles = [];
          currentTextParts = [];
          currentLines = 0;
        }
      });

      if (currentTextParts.length) {
        copyBlocks.push({
          files: currentFiles,
          text: currentTextParts.join('\n\n'),
          lineCount: currentLines,
          overLimit: currentLines > maxLines
        });
      }

      return copyBlocks;
    }

    function renderMergeBlocks(blocks, maxLines, totalFiles) {
      var container = document.getElementById('mergeBlocks');
      var summary = document.getElementById('mergeSummary');
      container.innerHTML = '';

      summary.textContent = 'Created ' + blocks.length + ' copy block(s) from ' + totalFiles + ' file(s). Max lines per block: ' + maxLines + '.';

      blocks.forEach(function(block, idx) {
        var blockNumber = idx + 1;
        var wrapper = document.createElement('div');
        wrapper.className = 'mergeBlock';

        var header = document.createElement('div');
        header.className = 'mergeBlockHeader';
        header.textContent = 'Copy Block ' + blockNumber + ' β€” ' + block.lineCount + ' line(s), ' + block.files.length + ' file(s)';
        if (block.overLimit) {
          header.textContent += ' β€” over max because one file is larger than the limit';
          header.className += ' mergeWarning';
        }

        var fileList = document.createElement('div');
        fileList.className = 'muted';
        fileList.textContent = 'Files: ' + block.files.join(', ');

        var textarea = document.createElement('textarea');
        textarea.className = 'mergeBox';
        textarea.id = 'mergeBox' + blockNumber;
        textarea.readOnly = true;
        textarea.value = block.text;

        var copyButton = document.createElement('button');
        copyButton.textContent = 'πŸ“‹ Copy Block ' + blockNumber;
        copyButton.onclick = function() {
          copyTextareaById(textarea.id, 'Copy Block ' + blockNumber + ' copied to clipboard!');
        };

        wrapper.appendChild(header);
        wrapper.appendChild(fileList);
        wrapper.appendChild(textarea);
        wrapper.appendChild(document.createElement('br'));
        wrapper.appendChild(copyButton);
        container.appendChild(wrapper);
      });
    }

    function readFileAsText(file) {
      return new Promise(function(resolve, reject) {
        var reader = new FileReader();
        reader.onload = function(e) { resolve(e.target.result || ''); };
        reader.onerror = reject;
        reader.readAsText(file);
      });
    }

    function countLines(text) {
      if (text === '') return 0;
      return String(text).split(/\r\n|\r|\n/).length;
    }

    function copyTextareaById(id, message) {
      var box = document.getElementById(id);
      box.select();
      document.execCommand('copy');
      alert(message || 'Copied to clipboard!');
    }

    function copyAllMergedBlocks() {
      var boxes = document.querySelectorAll('.mergeBox');
      if (!boxes.length) {
        alert('No merged blocks to copy.');
        return;
      }

      var allText = Array.prototype.map.call(boxes, function(box, idx) {
        return '===== COPY BLOCK ' + (idx + 1) + ' =====\n' + box.value;
      }).join('\n\n');

      var textarea = document.createElement('textarea');
      textarea.value = allText;
      document.body.appendChild(textarea);
      textarea.select();
      document.execCommand('copy');
      document.body.removeChild(textarea);
      alert('All merged blocks copied to clipboard!');
    }

    // ========== File Manager Logic ==========
    function refreshFileList() {
      google.script.run.withSuccessHandler(renderTable).getFileList();
    }

    function renderTable(data) {
      filesData = data;
      sortFiles();
      var tbody = document.getElementById('fileList');
      tbody.innerHTML = '';

      filesData.forEach(function(file) {
        var row = tbody.insertRow();
        var rawUrl = makeRawDriveUrl(file);
        row.innerHTML = '' +
          '<td><input type="checkbox" data-id="' + escapeAttr(file.id) + '" class="fileCheck"></td>' +
          '<td><a href="' + escapeAttr(rawUrl) + '" target="_blank">' + escapeHtml(file.name) + '</a></td>' +
          '<td>' + escapeHtml(file.mimeType) + '</td>' +
          '<td>' + formatBytes(file.size) + '</td>' +
          '<td>' + new Date(file.lastUpdated).toLocaleString() + '</td>';
      });
    }

    // Sorting
    Array.prototype.forEach.call(document.querySelectorAll('th[data-sort]'), function(th) {
      th.addEventListener('click', function() {
        var col = th.dataset.sort;
        if (sortColumn === col) {
          sortAsc = !sortAsc;
        } else {
          sortColumn = col;
          sortAsc = true;
        }
        Array.prototype.forEach.call(document.querySelectorAll('th'), function(h) {
          h.className = '';
        });
        th.classList.add(sortAsc ? 'sort-asc' : 'sort-desc');
        sortFiles();
        renderTable(filesData);
      });
    });

    function sortFiles() {
      filesData.sort(function(a, b) {
        var valA = a[sortColumn];
        var valB = b[sortColumn];
        if (sortColumn === 'size') {
          valA = Number(valA);
          valB = Number(valB);
        } else if (sortColumn === 'lastUpdated') {
          valA = new Date(valA).getTime();
          valB = new Date(valB).getTime();
        } else {
          valA = valA.toString().toLowerCase();
          valB = valB.toString().toLowerCase();
        }
        if (valA < valB) return sortAsc ? -1 : 1;
        if (valA > valB) return sortAsc ? 1 : -1;
        return 0;
      });
    }

    // Bulk delete
    function deleteSelected() {
      var checked = document.querySelectorAll('.fileCheck:checked');
      if (!checked.length) {
        alert('No files selected.');
        return;
      }
      if (!confirm('Delete ' + checked.length + ' file(s)? They will be moved to Trash.')) return;

      var ids = Array.prototype.map.call(checked, function(cb) {
        return cb.dataset.id;
      });
      google.script.run.withSuccessHandler(function() {
        refreshFileList();
        document.getElementById('selectAll').checked = false;
      }).deleteFiles(ids);
    }

    // Copy URLs of selected files
    function copySelectedUrls() {
      var checked = document.querySelectorAll('.fileCheck:checked');
      if (!checked.length) {
        alert('No files selected.');
        return;
      }
      // Gather URLs from filesData based on selected IDs
      var selectedIds = new Set(Array.prototype.map.call(checked, function(cb) {
        return cb.dataset.id;
      }));
      var urls = filesData
        .filter(function(file) {
          return selectedIds.has(file.id);
        })
        .map(function(file) {
          return makeRawDriveUrl(file);
        });
      if (urls.length === 0) {
        alert('No URLs found.');
        return;
      }
      // Create a temporary textarea to copy
      var textarea = document.createElement('textarea');
      textarea.value = urls.join('\n');
      document.body.appendChild(textarea);
      textarea.select();
      document.execCommand('copy');
      document.body.removeChild(textarea);
      alert('Copied ' + urls.length + ' URL(s) to clipboard!');
    }

    // Change sharing of selected files
    function changeSharingSelected() {
      var checked = document.querySelectorAll('.fileCheck:checked');
      if (!checked.length) {
        alert('No files selected.');
        return;
      }
      var action = confirm('Click OK to make these files PUBLIC (anyone with link can view),\nCancel to make them PRIVATE.');
      var ids = Array.prototype.map.call(checked, function(cb) {
        return cb.dataset.id;
      });
      google.script.run.withSuccessHandler(function() {
        refreshFileList();
        alert('Sharing updated for ' + ids.length + ' file(s).');
      }).changeSharing(ids, action); // true = public, false = private
    }

    function toggleAll(checkbox) {
      Array.prototype.forEach.call(document.querySelectorAll('.fileCheck'), function(cb) {
        cb.checked = checkbox.checked;
      });
    }

    function makeRawDriveUrl(fileOrUrl) {
      // If we get a file object from getFileList(), use its Drive file ID.
      if (typeof fileOrUrl === 'object' && fileOrUrl && fileOrUrl.id) {
        return 'https://drive.usercontent.google.com/download?id=' + encodeURIComponent(fileOrUrl.id) + '&export=download';
      }

      // If we get a normal Drive URL string from processUploads(), extract the ID.
      var url = String(fileOrUrl || '');

      // Already in the raw download format.
      if (url.indexOf('drive.usercontent.google.com/download') !== -1) {
        return url.indexOf('export=download') !== -1 ? url : url + '&export=download';
      }

      // Matches: https://drive.google.com/file/d/FILE_ID/view
      var match = url.match(/\/d\/([^/]+)/);
      if (match && match[1]) {
        return 'https://drive.usercontent.google.com/download?id=' + encodeURIComponent(match[1]) + '&export=download';
      }

      // Matches: https://drive.google.com/open?id=FILE_ID or any ?id=FILE_ID URL
      match = url.match(/[?&]id=([^&]+)/);
      if (match && match[1]) {
        return 'https://drive.usercontent.google.com/download?id=' + encodeURIComponent(match[1]) + '&export=download';
      }

      return url;
    }

    function escapeHtml(text) {
      var div = document.createElement('div');
      div.appendChild(document.createTextNode(text));
      return div.innerHTML;
    }

    function escapeAttr(text) {
      return String(text || '')
        .replace(/&/g, '&amp;')
        .replace(/"/g, '&quot;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;');
    }

    function formatBytes(bytes, decimals) {
      if (decimals === undefined) decimals = 2;
      if (!+bytes) return '0 Bytes';
      var k = 1024;
      var dm = decimals < 0 ? 0 : decimals;
      var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
      var i = Math.floor(Math.log(bytes) / Math.log(k));
      return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
    }
  </script>
</body>
</html>

πŸ“„ instructions.html

πŸ› οΈ How to Deploy Your Own

1. Click Create Apps Script Project above, or go to script.google.com and create a new project.
2. Copy the Code.gs content above and paste it into the default Code.gs file.
3. Create a new HTML file: File > New > HTML file, name it index. Paste the index.html code above.
4. Create another HTML file, name it instructions. Paste the instructions.html code above.
5. Click Deploy > New deployment, choose Web app.
6. Set Execute as: User accessing the web app. Do not choose β€œExecute as me” unless you understand that all Drive actions will run under your Google account.
7. Set Who has access to your preferred audience.
8. Before clicking Deploy, confirm again that Execute as says User accessing the web app.
9. Click Deploy and copy the web app URL.

Now upload the files to your Drive, manage them there, and get shareable URLs to send to DeepSeek. Tell DeepSeek something like β€œcheck these URLs,” not β€œview these uploaded files.” The wording matters.

πŸ”— Try the App

Click the button below to launch the file manager directly. You are on the instructions page right now.

⚠️ Never trust a script you didn't deploy yourself. If you are viewing this on someone else's deployment, you are about to grant that script access to your Drive. Always deploy your own copy from the code above, and make sure your deployment uses Execute as: User accessing the web app.

Made for transparency – share the code, deploy your own, stay safe.