Skip to content

Instantly share code, notes, and snippets.

@burak-kara
Last active February 13, 2026 22:49
Show Gist options
  • Select an option

  • Save burak-kara/b257525ea3ca781aa48982cc444fcdd0 to your computer and use it in GitHub Desktop.

Select an option

Save burak-kara/b257525ea3ca781aa48982cc444fcdd0 to your computer and use it in GitHub Desktop.
Google Drive: Unshare, Copy & Reclaim Ownership
/**
* =============================================================================
* Google Drive: Unshare, Copy & Reclaim Ownership
* =============================================================================
*
* PURPOSE:
* Processes a shared Google Drive folder tree and:
* 1. Removes all sharing permissions from files/folders you own.
* 2. Copies files you don't own into folders you do own, then removes
* the shared originals from your view.
* 3. Recreates non-owned folder structures as folders you own, preserving
* the hierarchy — including owned subfolders nested inside non-owned
* parent folders.
*
* USE CASE:
* You were shared a folder (e.g., from a former team or collaborator) and
* want to take full ownership of the content while removing all external
* access. Useful for offboarding, archiving shared projects, or reclaiming
* control of your Drive.
*
* HOW TO USE:
* 1. Set FOLDER_ID to the ID of the shared folder you want to process.
* (Find it in the folder's URL: drive.google.com/drive/folders/<FOLDER_ID>)
* 2. Run start() with DRY_RUN = true first to preview all changes in the log.
* 3. Review the log output carefully.
* 4. Set DRY_RUN = false and run start() again to execute.
*
* CONFIGURATION:
* - DRY_RUN: true = preview only (no changes made), false = execute changes.
* - COPY_NON_OWNED: If true, copies files you don't own into your folders.
* If false, skips non-owned files entirely.
* - RECREATE_NON_OWNED_FOLDERS: If true, recreates folder structures you
* don't own so the hierarchy is preserved under your ownership.
*
* LIMITATIONS & WARNINGS:
* - Google Apps Script has a ~6 minute execution limit. For very large folder
* trees, you may need to process subtrees individually or implement
* continuation tokens (not included here).
* - makeCopy() does not preserve comments, version history, or sharing
* settings from the original file. The copy is a clean snapshot.
* - Google Docs/Sheets/Slides copies are full copies. For other file types
* (PDFs, images, etc.), makeCopy() also works but verify for your use case.
* - removeFolder()/removeFile() removes the item from YOUR view (like
* removing a shortcut), it does NOT delete the original from the owner's
* Drive.
* - This script does NOT handle Google Shortcuts (.glink) — only real files
* and folders.
* - Always run with DRY_RUN = true first. The author is not responsible for
* any data loss.
*
* LICENSE: MIT — use at your own risk.
* =============================================================================
*/
const FOLDER_ID = "abcdefgh";
const DRY_RUN = true;
const COPY_NON_OWNED = true;
const RECREATE_NON_OWNED_FOLDERS = true;
function start() {
const folder = DriveApp.getFolderById(FOLDER_ID);
const stats = {
folders: 0,
files: 0,
errors: 0,
copied: 0,
moved: 0,
skippedFiles: 0,
removedFromView: 0,
foldersRecreated: 0,
foldersRemoved: 0
};
Logger.log(`=== ${DRY_RUN ? "DRY RUN" : "LIVE EXECUTION"} ===`);
Logger.log(`Target: ${folder.getName()} (${FOLDER_ID})`);
Logger.log(`Copy non-owned files: ${COPY_NON_OWNED}`);
Logger.log(`Recreate non-owned folders: ${RECREATE_NON_OWNED_FOLDERS}\n`);
processFolder(folder, stats, null, false);
Logger.log("\n=== Summary ===");
Logger.log(`Folders processed: ${stats.folders}`);
Logger.log(`Folders recreated (now owned by you): ${stats.foldersRecreated}`);
Logger.log(`Folders removed from your view: ${stats.foldersRemoved}`);
Logger.log(`Files processed: ${stats.files}`);
Logger.log(`Files copied (non-owned): ${stats.copied}`);
Logger.log(`Files moved (owned by you): ${stats.moved}`);
Logger.log(`Files removed from your view: ${stats.removedFromView}`);
Logger.log(`Files skipped: ${stats.skippedFiles}`);
Logger.log(`Errors: ${stats.errors}`);
}
/**
* Recursively processes a folder and its contents.
*
* @param {Folder} folder - The current folder being processed.
* @param {Object} stats - Running counters for the summary log.
* @param {Folder|null} parentFolder - The (possibly recreated) parent folder.
* null for the root target folder.
* @param {boolean} parentWasRecreated - Whether the parent folder was recreated
* in a previous recursion step. This flag ensures that owned subfolders
* inside non-owned parents are correctly relocated to the new parent,
* preventing them from being stranded when the original parent is removed.
*/
function processFolder(folder, stats, parentFolder, parentWasRecreated) {
stats.folders++;
const owner = folder.getOwner();
const ownerEmail = owner ? owner.getEmail() : "unknown";
const currentUserEmail = Session.getActiveUser().getEmail();
const isOwner = ownerEmail === currentUserEmail;
let targetFolder = folder;
let shouldRemoveOriginalFolder = false;
let thisWasRecreated = false;
// --- Case 1: Non-owned folder → recreate it under the parent so you own it ---
if (!isOwner && RECREATE_NON_OWNED_FOLDERS && parentFolder) {
Logger.log(`\n[Folder] ${folder.getName()}`);
Logger.log(` Owner: ${ownerEmail}`);
Logger.log(` You are owner: false`);
Logger.log(` → Creating owned copy of folder`);
if (!DRY_RUN) {
targetFolder = parentFolder.createFolder(folder.getName());
Logger.log(` ✓ Folder created: ${targetFolder.getId()}`);
stats.foldersRecreated++;
resetPermissions(targetFolder, "Folder (New)", stats);
} else {
Logger.log(` [DRY RUN] Would create owned folder`);
stats.foldersRecreated++;
}
shouldRemoveOriginalFolder = true;
thisWasRecreated = true;
// --- Case 2: Owned folder, but parent was recreated → must relocate ---
// Without this, owned subfolders inside non-owned parents would be stranded
// when the original non-owned parent is removed from your view.
} else if (isOwner && parentWasRecreated && parentFolder) {
Logger.log(`\n[Folder] ${folder.getName()}`);
Logger.log(` Owner: ${ownerEmail}`);
Logger.log(` You are owner: true`);
Logger.log(` → Parent was recreated, relocating owned folder`);
if (!DRY_RUN) {
targetFolder = parentFolder.createFolder(folder.getName());
Logger.log(` ✓ Folder created under new parent: ${targetFolder.getId()}`);
stats.foldersRecreated++;
resetPermissions(targetFolder, "Folder (Relocated)", stats);
} else {
Logger.log(` [DRY RUN] Would relocate owned folder under new parent`);
stats.foldersRecreated++;
}
shouldRemoveOriginalFolder = true;
thisWasRecreated = true;
}
// --- Process all files in this folder ---
const fileList = [];
const files = folder.getFiles();
while (files.hasNext()) {
fileList.push(files.next());
}
fileList.forEach(file => {
stats.files++;
processFile(file, folder, targetFolder, stats);
});
// --- Recurse into subfolders, passing down the recreated flag ---
const subfolderList = [];
const subfolders = folder.getFolders();
while (subfolders.hasNext()) {
subfolderList.push(subfolders.next());
}
subfolderList.forEach(subfolder => {
processFolder(subfolder, stats, targetFolder, thisWasRecreated);
});
// --- Clean up: remove original folder from view, or reset permissions ---
if (shouldRemoveOriginalFolder && parentFolder) {
if (!DRY_RUN) {
parentFolder.removeFolder(folder);
stats.foldersRemoved++;
Logger.log(` ✓ Original folder "${folder.getName()}" removed from your view`);
} else {
Logger.log(` [DRY RUN] Would remove original folder "${folder.getName()}" from your view`);
stats.foldersRemoved++;
}
} else if (isOwner) {
// Folder is owned and stays in place — just strip sharing permissions
processFolderPermissions(folder, stats);
}
}
/**
* Processes a single file: moves owned files, copies non-owned files,
* and resets permissions on the result.
*
* @param {File} file - The file to process.
* @param {Folder} sourceFolder - The original folder containing the file.
* @param {Folder} targetFolder - The destination folder (may be the same as
* sourceFolder if no recreation occurred).
* @param {Object} stats - Running counters for the summary log.
*/
function processFile(file, sourceFolder, targetFolder, stats) {
try {
const name = file.getName();
const owner = file.getOwner();
const ownerEmail = owner ? owner.getEmail() : "unknown";
const currentUserEmail = Session.getActiveUser().getEmail();
const isOwner = ownerEmail === currentUserEmail;
Logger.log(`\n[File] ${name}`);
Logger.log(` Owner: ${ownerEmail}`);
Logger.log(` You are owner: ${isOwner}`);
if (sourceFolder.getId() !== targetFolder.getId()) {
// Parent folder was recreated — files need to move to the new target
if (isOwner) {
// You own it: move it to the new folder
Logger.log(` → Moving to new folder (you own it)`);
if (!DRY_RUN) {
targetFolder.addFile(file);
sourceFolder.removeFile(file);
stats.moved++;
Logger.log(` ✓ File moved to new folder`);
resetPermissions(file, "File (Moved)", stats);
} else {
Logger.log(` [DRY RUN] Would move file to new folder`);
stats.moved++;
}
} else if (COPY_NON_OWNED) {
// You don't own it: make a copy you own, remove the shared original
Logger.log(` → Creating copy in new folder`);
if (!DRY_RUN) {
const copy = file.makeCopy(name, targetFolder);
Logger.log(` ✓ Copy created: ${copy.getId()}`);
stats.copied++;
resetPermissions(copy, "File (Copy)", stats);
sourceFolder.removeFile(file);
stats.removedFromView++;
Logger.log(` ✓ Original removed from your view`);
} else {
Logger.log(` [DRY RUN] Would create copy and remove original`);
stats.copied++;
stats.removedFromView++;
}
} else {
Logger.log(` ⊘ SKIPPED: Not owner and COPY_NON_OWNED=false`);
stats.skippedFiles++;
}
} else {
// Folder was not recreated — file stays in place
if (isOwner) {
// Just strip sharing permissions
resetPermissions(file, "File", stats);
} else if (COPY_NON_OWNED) {
// Copy in place, then remove the shared original
Logger.log(` → Creating copy`);
if (!DRY_RUN) {
const copy = file.makeCopy(name, targetFolder);
Logger.log(` ✓ Copy created: ${copy.getId()}`);
stats.copied++;
resetPermissions(copy, "File (Copy)", stats);
sourceFolder.removeFile(file);
stats.removedFromView++;
Logger.log(` ✓ Original removed from your view`);
} else {
Logger.log(` [DRY RUN] Would create copy and remove original`);
stats.copied++;
stats.removedFromView++;
}
} else {
Logger.log(` ⊘ SKIPPED: Not owner and COPY_NON_OWNED=false`);
stats.skippedFiles++;
}
}
} catch (e) {
stats.errors++;
Logger.log(` ✗ ERROR processing file: ${e.toString()}`);
}
}
/**
* Resets permissions on an owned folder (called only when the folder
* was not recreated and stays in place).
*/
function processFolderPermissions(folder, stats) {
try {
const owner = folder.getOwner();
const ownerEmail = owner ? owner.getEmail() : "unknown";
const currentUserEmail = Session.getActiveUser().getEmail();
const isOwner = ownerEmail === currentUserEmail;
if (isOwner) {
resetPermissions(folder, "Folder", stats);
} else {
Logger.log(`\n[Folder] ${folder.getName()}`);
Logger.log(` Owner: ${ownerEmail}`);
Logger.log(` ⊘ SKIPPED: You don't own this folder`);
}
} catch (e) {
stats.errors++;
Logger.log(` ✗ ERROR processing folder: ${e.toString()}`);
}
}
/**
* Removes all sharing permissions from a file or folder.
* Revokes link sharing, removes all editors and viewers.
* Only call this on assets you own — otherwise it will throw.
*/
function resetPermissions(asset, type, stats) {
try {
const name = asset.getName();
const owner = asset.getOwner();
const ownerEmail = owner ? owner.getEmail() : "unknown";
Logger.log(`\n[${type}] ${name}`);
Logger.log(` Owner: ${ownerEmail}`);
const editors = asset.getEditors();
const viewers = asset.getViewers();
const sharingAccess = asset.getSharingAccess();
const sharingPermission = asset.getSharingPermission();
Logger.log(` Current sharing: ${sharingAccess} / ${sharingPermission}`);
Logger.log(` Editors (${editors.length}): ${editors.map(e => e.getEmail()).join(", ") || "none"}`);
Logger.log(` Viewers (${viewers.length}): ${viewers.map(v => v.getEmail()).join(", ") || "none"}`);
if (DRY_RUN) {
Logger.log(" [DRY RUN] Would remove all permissions");
return;
}
// Revoke link-based sharing
asset.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.NONE);
asset.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.NONE);
// Remove individual editors
editors.forEach(editor => {
const email = editor.getEmail();
if (email) {
asset.removeEditor(email);
} else {
Logger.log(` ⚠ Warning: Editor with no email, skipped`);
}
});
// Remove individual viewers
viewers.forEach(viewer => {
const email = viewer.getEmail();
if (email) {
asset.removeViewer(email);
} else {
Logger.log(` ⚠ Warning: Viewer with no email, skipped`);
}
});
Logger.log(" ✓ Permissions removed");
} catch (e) {
stats.errors++;
Logger.log(` ✗ ERROR resetting permissions: ${e.toString()}`);
}
}
Copy link

ghost commented Jun 14, 2022

Tutorial please. I am new in this thing.

@burak-kara
Copy link
Author

burak-kara commented Jun 15, 2022

Tutorial please. I am new in this thing.

I believe you can find one on YouTube. The description states that Google Apps Script commands can be changed after this version. Therefore, this script might not be working

@mmansion
Copy link

Great starting point thank you

@JakubAndrysek
Copy link

Excellent, thanks 😍

@HackettLai
Copy link

You are just a genius!! Thx x 1000000000

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment