Last active
February 13, 2026 22:49
-
-
Save burak-kara/b257525ea3ca781aa48982cc444fcdd0 to your computer and use it in GitHub Desktop.
Google Drive: Unshare, Copy & Reclaim Ownership
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * ============================================================================= | |
| * 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()}`); | |
| } | |
| } |
Author
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
Great starting point thank you
Excellent, thanks 😍
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
Tutorial please. I am new in this thing.