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.
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.
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.
Web Uploads in your Google Drive, and raw download URLs appear in a box.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.
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) {}
});
}
<!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, '&')
.replace(/"/g, '"')
.replace(/</g, '<')
.replace(/>/g, '>');
}
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>
Code.gs content above and paste it into the default Code.gs file.File > New > HTML file, name it index. Paste the index.html code above.instructions. Paste the instructions.html code above.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.
Click the button below to launch the file manager directly. You are on the instructions page right now.
Made for transparency β share the code, deploy your own, stay safe.