Skip to content

Instantly share code, notes, and snippets.

@71
Created May 5, 2022 00:53
Show Gist options
  • Save 71/6384f7d999514b4583e64e2d6e023e7a to your computer and use it in GitHub Desktop.
Save 71/6384f7d999514b4583e64e2d6e023e7a to your computer and use it in GitHub Desktop.
Generate a "Tags" folder in Google Drive with tags from all files.
function main() {
buildTagsDirectory(
DriveApp.getRootFolder(),
DriveApp.getFolderById("..."),
);
}
/**
* Builds a recursive tags structure in `tagsFolder` with all tags found in items in `rootFolder`.
*/
function buildTagsDirectory(
/** @type DriveApp.Folder */ rootFolder,
/** @type DriveApp.Folder */ tagsFolder,
) {
// Find all items belonging to each tag.
/** @type Map<string, { item: DriveApp.Folder | DriveApp.File, tags: Set<string> } | null> */
const itemsById = new Map([[tagsFolder.getId(), null]]);
readAll(itemsById);
/** @type Map<string, { item: DriveApp.Folder | DriveApp.File, tags: Set<string> }[]> */
const itemsByTag = new Map();
for (const itemAndTags of itemsById.values()) {
if (itemAndTags === null) {
continue;
}
for (const tag of itemAndTags.tags) {
let items = itemsByTag.get(tag);
if (items === undefined) {
itemsByTag.set(tag, items = []);
}
items.push(itemAndTags);
}
}
// Create tag directory.
createTagDirectories(() => tagsFolder, itemsByTag, new Set());
}
/**
* Stores all files and folders in Drive along with their tags into `results`.
*/
function readAll(
/** @type Map<string, { item: DriveApp.Folder | DriveApp.File, tags: Set<string> } | null> */ results,
) {
for (const files = DriveApp.getFiles(); files.hasNext();) {
const file = files.next(),
fileId = file.getId();
if (results.has(fileId) || file.isTrashed()) {
continue;
}
const fileTags = extractTags(file.getDescription());
results.set(
fileId,
fileTags.size > 0 ? { item: file, tags: fileTags } : null,
);
}
for (const folders = DriveApp.getFolders(); folders.hasNext();) {
const folder = folders.next(),
folderId = folder.getId();
if (results.has(folderId) || folder.isTrashed()) {
return;
}
const folderTags = extractTags(folder.getDescription());
results.set(
folderId,
folderTags.size > 0 ? { item: folder, tags: folderTags } : null,
);
}
}
/**
* Recursively creates the directories for the given tags.
*/
function createTagDirectories(
/** @type {(force: boolean) => DriveApp.Folder | undefined} */ getParentFolder,
/** @type Map<string, { item: DriveApp.Folder | DriveApp.File, tags: Set<string> }[]> */ allTags,
/** @type Set<string> */ currentTags,
/** @type {string | undefined} */ currentTag,
) {
const existingParentFolder = getParentFolder(false);
if (currentTag !== undefined) {
// Save existing shortcuts to avoid recreating shortcuts.
/** @type Map<string, DriveApp.File> */
const existingShortcuts = new Map();
if (existingParentFolder !== undefined) {
for (const files = existingParentFolder.getFiles(); files.hasNext();) {
const file = files.next(),
targetId = file.getTargetId(),
isShortcut = targetId !== null;
if (isShortcut) {
existingShortcuts.set(targetId, file);
}
}
}
// Create shortcuts to items.
const items = allTags.get(currentTag);
outer: for (const { item, tags } of items) {
for (const tag of currentTags) {
if (!tags.has(tag)) {
continue outer;
}
}
const itemId = item.getId(),
file = existingShortcuts.get(itemId);
if (file !== undefined) {
existingShortcuts.delete(itemId);
file.setTrashed(false);
} else {
getParentFolder(true).createShortcut(itemId);
}
}
// Delete unused existing shortcuts.
for (const file of existingShortcuts.values()) {
file.setTrashed(true);
}
}
// Save existing folders to avoid recreating folders.
/** @type Map<string, DriveApp.Folder> */
const existingFolders = new Map();
if (existingParentFolder !== undefined) {
for (const folders = existingParentFolder.getFolders(); folders.hasNext();) {
const folder = folders.next();
existingFolders.set(folder.getId(), folder);
}
}
// Create nested tags.
for (const tag of allTags.keys()) {
if (currentTags.has(tag)) {
continue;
}
let folder = existingFolders.get(tag),
hasExistingFolder = folder !== undefined;
function getFolder(/** @type boolean */ force) {
if (!force) {
return folder;
}
if (folder !== undefined) {
if (hasExistingFolder) {
// Make sure to mark that the folder has been used.
existingFolders.delete(tag);
folder.setTrashed(false);
hasExistingFolder = false;
}
return folder;
}
// Folder does not exist but has been requested, create it.
return folder = getParentFolder(true).createFolder(tag);
}
currentTags.add(tag);
createTagDirectories(getFolder, allTags, currentTags, tag);
currentTags.delete(tag);
}
// Delete unused existing folders.
for (const folder of existingFolders.values()) {
folder.setTrashed(true);
}
}
/**
* Returns the list of all the tags found in the given description.
*/
function extractTags(/** @type string */ description) {
/** @type Set<string> */
const results = new Set();
for (const re = /\[\[(.+?)\]\]/g;;) {
const match = re.exec(description);
if (match === null) {
return results;
}
results.add(match[1]);
}
}
/**
* Returns `true` if the given `set` has the given `element`, else
* adds the element to the set and returns `false`.
*
* @template T
*/
function hasOrAdd(/** @type Set<T> */ set, /** @type T */ element) {
if (set.has(element)) {
return true;
}
set.add(element);
return false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment