Created
February 27, 2026 21:26
-
-
Save chhoumann/5152bf85ce9c7aa4e51285e04702f6fc to your computer and use it in GitHub Desktop.
QuickAdd safe duplicate-folder macro script and package for issue #785
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
| { | |
| "schemaVersion": 1, | |
| "quickAddVersion": "2.11.0", | |
| "createdAt": "2026-02-27T21:25:54.977Z", | |
| "rootChoiceIds": [ | |
| "qa-macro-duplicate-folder-v1" | |
| ], | |
| "choices": [ | |
| { | |
| "choice": { | |
| "id": "qa-macro-duplicate-folder-v1", | |
| "name": "Duplicate Folder", | |
| "type": "Macro", | |
| "command": true, | |
| "runOnStartup": false, | |
| "macro": { | |
| "id": "qa-macro-duplicate-folder-v1-macro", | |
| "name": "Duplicate Folder", | |
| "commands": [ | |
| { | |
| "id": "qa-command-duplicate-folder-script-v1", | |
| "name": "Duplicate Folder Script", | |
| "type": "UserScript", | |
| "path": "Scripts/duplicateFolder.js", | |
| "settings": {} | |
| } | |
| ] | |
| } | |
| }, | |
| "pathHint": [ | |
| "Duplicate Folder" | |
| ], | |
| "parentChoiceId": null | |
| } | |
| ], | |
| "assets": [ | |
| { | |
| "kind": "user-script", | |
| "originalPath": "Scripts/duplicateFolder.js", | |
| "contentEncoding": "base64", | |
| "content": "LyoqCiAqIER1cGxpY2F0ZSBhIGZvbGRlciAoaW5jbHVkaW5nIHN1YmZvbGRlcnMgYW5kIGZpbGVzKSBpbnNpZGUgdGhlIGN1cnJlbnQgdmF1bHQuCiAqCiAqIFNhZmUgZGVmYXVsdHM6CiAqIC0gTmV2ZXIgb3ZlcndyaXRlcyBleGlzdGluZyBmaWxlcy4KICogLSBCbG9ja3Mgb3ZlcmxhcHBpbmcgc291cmNlL2Rlc3RpbmF0aW9uIHBhdGhzLgogKiAtIE9wdGlvbmFsIGRyeS1ydW4gbW9kZS4KICogLSBPcHRpb25hbCBtYXJrZG93biB0ZXh0IHJlcGxhY2VtZW50IGZvciB0ZW1wbGF0aW5nLgogKi8KbW9kdWxlLmV4cG9ydHMgPSB7CiAgICBlbnRyeTogc3RhcnQsCiAgICBzZXR0aW5nczogewogICAgICAgIG5hbWU6ICJEdXBsaWNhdGUgRm9sZGVyIiwKICAgICAgICBhdXRob3I6ICJRdWlja0FkZCIsCiAgICAgICAgb3B0aW9uczogewogICAgICAgICAgICAiRGVmYXVsdCBTb3VyY2UgRm9sZGVyIjogewogICAgICAgICAgICAgICAgdHlwZTogInRleHQiLAogICAgICAgICAgICAgICAgZGVmYXVsdFZhbHVlOiAiIiwKICAgICAgICAgICAgICAgIHBsYWNlaG9sZGVyOiAiUHJvamVjdHMvVGVtcGxhdGUiLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJPcHRpb25hbCBkZWZhdWx0IHNvdXJjZSBmb2xkZXIgdG8gcHJlc2VsZWN0IgogICAgICAgICAgICB9LAogICAgICAgICAgICAiRGVzdGluYXRpb24gU3VmZml4IjogewogICAgICAgICAgICAgICAgdHlwZTogInRleHQiLAogICAgICAgICAgICAgICAgZGVmYXVsdFZhbHVlOiAiIChDb3B5KSIsCiAgICAgICAgICAgICAgICBwbGFjZWhvbGRlcjogIiAoQ29weSkiLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJEZWZhdWx0IHN1ZmZpeCBhZGRlZCB0byB0aGUgc3VnZ2VzdGVkIGRlc3RpbmF0aW9uIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiQWxsb3cgRXhpc3RpbmcgRGVzdGluYXRpb24gRm9sZGVyIjogewogICAgICAgICAgICAgICAgdHlwZTogInRvZ2dsZSIsCiAgICAgICAgICAgICAgICBkZWZhdWx0VmFsdWU6IGZhbHNlLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJJZiBkaXNhYmxlZCwgYWJvcnQgd2hlbiBkZXN0aW5hdGlvbiBmb2xkZXIgYWxyZWFkeSBleGlzdHMiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJTa2lwIEV4aXN0aW5nIEZpbGVzIjogewogICAgICAgICAgICAgICAgdHlwZTogInRvZ2dsZSIsCiAgICAgICAgICAgICAgICBkZWZhdWx0VmFsdWU6IGZhbHNlLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJJZiBlbmFibGVkLCBza2lwIGZpbGUgY29sbGlzaW9ucyBpbnN0ZWFkIG9mIGFib3J0aW5nIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiRmluZCBUZXh0IGluIE1hcmtkb3duIjogewogICAgICAgICAgICAgICAgdHlwZTogInRleHQiLAogICAgICAgICAgICAgICAgZGVmYXVsdFZhbHVlOiAiIiwKICAgICAgICAgICAgICAgIHBsYWNlaG9sZGVyOiAiT2xkIFByb2plY3QgTmFtZSIsCiAgICAgICAgICAgICAgICBkZXNjcmlwdGlvbjogIk9wdGlvbmFsIHBsYWluIHRleHQgdG8gcmVwbGFjZSBpbiAubWQgZmlsZXMiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJSZXBsYWNlIFRleHQgaW4gTWFya2Rvd24iOiB7CiAgICAgICAgICAgICAgICB0eXBlOiAidGV4dCIsCiAgICAgICAgICAgICAgICBkZWZhdWx0VmFsdWU6ICIiLAogICAgICAgICAgICAgICAgcGxhY2Vob2xkZXI6ICJOZXcgUHJvamVjdCBOYW1lIiwKICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uOiAiUmVwbGFjZW1lbnQgdGV4dCBmb3IgbWFya2Rvd24gbm90ZXMiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJEcnkgUnVuIjogewogICAgICAgICAgICAgICAgdHlwZTogInRvZ2dsZSIsCiAgICAgICAgICAgICAgICBkZWZhdWx0VmFsdWU6IGZhbHNlLAogICAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJQcmV2aWV3IGFjdGlvbnMgd2l0aG91dCB3cml0aW5nIGZpbGVzIgogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfQp9OwoKYXN5bmMgZnVuY3Rpb24gc3RhcnQocGFyYW1zLCBzZXR0aW5ncykgewogICAgY29uc3QgeyBhcHAsIG9ic2lkaWFuLCBxdWlja0FkZEFwaSwgdmFyaWFibGVzIH0gPSBwYXJhbXM7CiAgICBjb25zdCB7IE5vdGljZSwgVEZpbGUsIFRGb2xkZXIsIG5vcm1hbGl6ZVBhdGggfSA9IG9ic2lkaWFuOwoKICAgIGNvbnN0IHNvdXJjZUZvbGRlclBhdGggPSBhd2FpdCBzZWxlY3RTb3VyY2VGb2xkZXIoCiAgICAgICAgYXBwLAogICAgICAgIHF1aWNrQWRkQXBpLAogICAgICAgIHNldHRpbmdzWyJEZWZhdWx0IFNvdXJjZSBGb2xkZXIiXSwKICAgICAgICBub3JtYWxpemVQYXRoCiAgICApOwogICAgaWYgKCFzb3VyY2VGb2xkZXJQYXRoKSB7CiAgICAgICAgbmV3IE5vdGljZSgiRm9sZGVyIGR1cGxpY2F0aW9uIGNhbmNlbGxlZC4iKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgY29uc3Qgc291cmNlRm9sZGVyID0gYXBwLnZhdWx0LmdldEFic3RyYWN0RmlsZUJ5UGF0aChzb3VyY2VGb2xkZXJQYXRoKTsKICAgIGlmICghKHNvdXJjZUZvbGRlciBpbnN0YW5jZW9mIFRGb2xkZXIpKSB7CiAgICAgICAgbmV3IE5vdGljZShgU291cmNlIGZvbGRlciBkb2VzIG5vdCBleGlzdDogJHtzb3VyY2VGb2xkZXJQYXRofWApOwogICAgICAgIHJldHVybjsKICAgIH0KCiAgICBjb25zdCBzdWdnZXN0ZWREZXN0aW5hdGlvbiA9IG5vcm1hbGl6ZVBhdGgoCiAgICAgICAgYCR7c291cmNlRm9sZGVyUGF0aH0ke3NldHRpbmdzWyJEZXN0aW5hdGlvbiBTdWZmaXgiXSB8fCAiIChDb3B5KSJ9YAogICAgKTsKICAgIGNvbnN0IGRlc3RpbmF0aW9uSW5wdXQgPSBhd2FpdCBxdWlja0FkZEFwaS5pbnB1dFByb21wdCgKICAgICAgICAiRGVzdGluYXRpb24gZm9sZGVyIiwKICAgICAgICAiV2hlcmUgc2hvdWxkIHRoZSBjb3B5IGJlIGNyZWF0ZWQ/IiwKICAgICAgICBzdWdnZXN0ZWREZXN0aW5hdGlvbgogICAgKTsKCiAgICBpZiAoIWRlc3RpbmF0aW9uSW5wdXQgfHwgIWRlc3RpbmF0aW9uSW5wdXQudHJpbSgpKSB7CiAgICAgICAgbmV3IE5vdGljZSgiRm9sZGVyIGR1cGxpY2F0aW9uIGNhbmNlbGxlZC4iKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgY29uc3QgZGVzdGluYXRpb25Gb2xkZXJQYXRoID0gbm9ybWFsaXplUGF0aChkZXN0aW5hdGlvbklucHV0LnRyaW0oKSk7CgogICAgaWYgKCFpc1NhZmVWYXVsdFBhdGgoZGVzdGluYXRpb25Gb2xkZXJQYXRoKSkgewogICAgICAgIG5ldyBOb3RpY2UoIkRlc3RpbmF0aW9uIHBhdGggaXMgaW52YWxpZC4iKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgaWYgKGRlc3RpbmF0aW9uRm9sZGVyUGF0aCA9PT0gc291cmNlRm9sZGVyUGF0aCkgewogICAgICAgIG5ldyBOb3RpY2UoIkRlc3RpbmF0aW9uIGNhbm5vdCBiZSB0aGUgc2FtZSBhcyBzb3VyY2UuIik7CiAgICAgICAgcmV0dXJuOwogICAgfQoKICAgIGlmICgKICAgICAgICBpc1N1YlBhdGgoc291cmNlRm9sZGVyUGF0aCwgZGVzdGluYXRpb25Gb2xkZXJQYXRoKSB8fAogICAgICAgIGlzU3ViUGF0aChkZXN0aW5hdGlvbkZvbGRlclBhdGgsIHNvdXJjZUZvbGRlclBhdGgpCiAgICApIHsKICAgICAgICBuZXcgTm90aWNlKAogICAgICAgICAgICAiU291cmNlIGFuZCBkZXN0aW5hdGlvbiBmb2xkZXJzIGNhbm5vdCBvdmVybGFwIChwYXJlbnQvY2hpbGQpLiIKICAgICAgICApOwogICAgICAgIHJldHVybjsKICAgIH0KCiAgICBjb25zdCBhbGxvd0V4aXN0aW5nRGVzdGluYXRpb24gPSAhIXNldHRpbmdzWyJBbGxvdyBFeGlzdGluZyBEZXN0aW5hdGlvbiBGb2xkZXIiXTsKICAgIGNvbnN0IHNraXBFeGlzdGluZ0ZpbGVzID0gISFzZXR0aW5nc1siU2tpcCBFeGlzdGluZyBGaWxlcyJdOwogICAgY29uc3QgZHJ5UnVuID0gISFzZXR0aW5nc1siRHJ5IFJ1biJdOwoKICAgIGNvbnN0IGRlc3RpbmF0aW9uRXhpc3RzID0gYXdhaXQgYXBwLnZhdWx0LmFkYXB0ZXIuZXhpc3RzKGRlc3RpbmF0aW9uRm9sZGVyUGF0aCk7CiAgICBpZiAoZGVzdGluYXRpb25FeGlzdHMgJiYgIWFsbG93RXhpc3RpbmdEZXN0aW5hdGlvbikgewogICAgICAgIG5ldyBOb3RpY2UoCiAgICAgICAgICAgIGBEZXN0aW5hdGlvbiBhbHJlYWR5IGV4aXN0czogJHtkZXN0aW5hdGlvbkZvbGRlclBhdGh9LiBgICsKICAgICAgICAgICAgIkVuYWJsZSAnQWxsb3cgRXhpc3RpbmcgRGVzdGluYXRpb24gRm9sZGVyJyB0byBjb250aW51ZS4iCiAgICAgICAgKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgY29uc3Qgc2NhbiA9IHNjYW5Gb2xkZXIoc291cmNlRm9sZGVyLCBURm9sZGVyLCBURmlsZSk7CiAgICBjb25zdCBwbGFuID0gYnVpbGRQbGFuKAogICAgICAgIHNvdXJjZUZvbGRlclBhdGgsCiAgICAgICAgZGVzdGluYXRpb25Gb2xkZXJQYXRoLAogICAgICAgIHNjYW4uZm9sZGVycywKICAgICAgICBzY2FuLmZpbGVzLAogICAgICAgIG5vcm1hbGl6ZVBhdGgKICAgICk7CgogICAgaWYgKHBsYW4uaW52YWxpZFBhdGhzLmxlbmd0aCA+IDApIHsKICAgICAgICBuZXcgTm90aWNlKAogICAgICAgICAgICBgQWJvcnRlZCBkdWUgdG8gaW52YWxpZCBwYXRoIG1hcHBpbmc6ICR7cGxhbi5pbnZhbGlkUGF0aHNbMF19YAogICAgICAgICk7CiAgICAgICAgcmV0dXJuOwogICAgfQoKICAgIGNvbnN0IGV4aXN0aW5nVGFyZ2V0cyA9IGF3YWl0IGZpbmRFeGlzdGluZ1RhcmdldHMoYXBwLCBwbGFuKTsKICAgIGlmIChleGlzdGluZ1RhcmdldHMuZmlsZVBhdGhzLmxlbmd0aCA+IDAgJiYgIXNraXBFeGlzdGluZ0ZpbGVzKSB7CiAgICAgICAgbmV3IE5vdGljZSgKICAgICAgICAgICAgYEFib3J0ZWQ6ICR7ZXhpc3RpbmdUYXJnZXRzLmZpbGVQYXRocy5sZW5ndGh9IGZpbGUocykgYWxyZWFkeSBleGlzdCBpbiBkZXN0aW5hdGlvbi5gCiAgICAgICAgKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgY29uc3QgZmluZFRleHQgPSBgJHtzZXR0aW5nc1siRmluZCBUZXh0IGluIE1hcmtkb3duIl0gfHwgIiJ9YDsKICAgIGNvbnN0IHJlcGxhY2VUZXh0ID0gYCR7c2V0dGluZ3NbIlJlcGxhY2UgVGV4dCBpbiBNYXJrZG93biJdIHx8ICIifWA7CiAgICBjb25zdCBzaG91bGRSZXBsYWNlSW5NYXJrZG93biA9IGZpbmRUZXh0Lmxlbmd0aCA+IDA7CgogICAgY29uc3QgcHJldmlld0xpbmVzID0gWwogICAgICAgIGBTb3VyY2U6ICR7c291cmNlRm9sZGVyUGF0aH1gLAogICAgICAgIGBEZXN0aW5hdGlvbjogJHtkZXN0aW5hdGlvbkZvbGRlclBhdGh9YCwKICAgICAgICBgRm9sZGVycyB0byBjcmVhdGU6ICR7cGxhbi5mb2xkZXJQYXRocy5sZW5ndGh9YCwKICAgICAgICBgRmlsZXMgdG8gY29weTogJHtwbGFuLmZpbGVQbGFucy5sZW5ndGh9YCwKICAgICAgICBgRXhpc3RpbmcgZmlsZXM6ICR7ZXhpc3RpbmdUYXJnZXRzLmZpbGVQYXRocy5sZW5ndGh9YCwKICAgICAgICBgRHJ5IHJ1bjogJHtkcnlSdW4gPyAieWVzIiA6ICJubyJ9YCwKICAgIF07CgogICAgaWYgKHNob3VsZFJlcGxhY2VJbk1hcmtkb3duKSB7CiAgICAgICAgcHJldmlld0xpbmVzLnB1c2goCiAgICAgICAgICAgIGBNYXJrZG93biByZXBsYWNlbWVudDogXCIke2ZpbmRUZXh0fVwiIOKGkiBcIiR7cmVwbGFjZVRleHR9XCJgCiAgICAgICAgKTsKICAgIH0KCiAgICBjb25zdCBzaG91bGRDb250aW51ZSA9IGF3YWl0IHF1aWNrQWRkQXBpLnllc05vUHJvbXB0KAogICAgICAgICJEdXBsaWNhdGUgZm9sZGVyPyIsCiAgICAgICAgcHJldmlld0xpbmVzLmpvaW4oIlxuIikKICAgICk7CiAgICBpZiAoIXNob3VsZENvbnRpbnVlKSB7CiAgICAgICAgbmV3IE5vdGljZSgiRm9sZGVyIGR1cGxpY2F0aW9uIGNhbmNlbGxlZC4iKTsKICAgICAgICByZXR1cm47CiAgICB9CgogICAgbGV0IHJlc3VsdDsKICAgIHRyeSB7CiAgICAgICAgcmVzdWx0ID0gYXdhaXQgZXhlY3V0ZVBsYW4oewogICAgICAgICAgICBhcHAsCiAgICAgICAgICAgIHBsYW4sCiAgICAgICAgICAgIGV4aXN0aW5nVGFyZ2V0cywKICAgICAgICAgICAgc2tpcEV4aXN0aW5nRmlsZXMsCiAgICAgICAgICAgIHNob3VsZFJlcGxhY2VJbk1hcmtkb3duLAogICAgICAgICAgICBmaW5kVGV4dCwKICAgICAgICAgICAgcmVwbGFjZVRleHQsCiAgICAgICAgICAgIGRyeVJ1biwKICAgICAgICB9KTsKICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgY29uc3QgbWVzc2FnZSA9IGVycm9yPy5tZXNzYWdlIHx8IGAke2Vycm9yfWA7CiAgICAgICAgbmV3IE5vdGljZShgRm9sZGVyIGR1cGxpY2F0aW9uIGZhaWxlZDogJHttZXNzYWdlfWAsIDgwMDApOwogICAgICAgIHRocm93IGVycm9yOwogICAgfQoKICAgIHZhcmlhYmxlcy5kdXBsaWNhdGVkU291cmNlRm9sZGVyID0gc291cmNlRm9sZGVyUGF0aDsKICAgIHZhcmlhYmxlcy5kdXBsaWNhdGVkRGVzdGluYXRpb25Gb2xkZXIgPSBkZXN0aW5hdGlvbkZvbGRlclBhdGg7CiAgICB2YXJpYWJsZXMuZHVwbGljYXRlZEZvbGRlcnNDcmVhdGVkID0gcmVzdWx0LmZvbGRlcnNDcmVhdGVkOwogICAgdmFyaWFibGVzLmR1cGxpY2F0ZWRGaWxlc0NvcGllZCA9IHJlc3VsdC5maWxlc0NvcGllZDsKICAgIHZhcmlhYmxlcy5kdXBsaWNhdGVkRmlsZXNTa2lwcGVkID0gcmVzdWx0LmZpbGVzU2tpcHBlZDsKICAgIHZhcmlhYmxlcy5kdXBsaWNhdGVkTWFya2Rvd25GaWxlc1VwZGF0ZWQgPSByZXN1bHQubWFya2Rvd25GaWxlc1VwZGF0ZWQ7CgogICAgY29uc3Qgc3VtbWFyeSA9IGRyeVJ1bgogICAgICAgID8gYERyeSBydW4gY29tcGxldGUuIFBsYW5uZWQgJHtwbGFuLmZvbGRlclBhdGhzLmxlbmd0aH0gZm9sZGVyKHMpIGFuZCAke3BsYW4uZmlsZVBsYW5zLmxlbmd0aH0gZmlsZShzKS5gCiAgICAgICAgOiBgRHVwbGljYXRlZCBmb2xkZXIgc3VjY2Vzc2Z1bGx5LiBDcmVhdGVkICR7cmVzdWx0LmZvbGRlcnNDcmVhdGVkfSBmb2xkZXIocyksIGNvcGllZCAke3Jlc3VsdC5maWxlc0NvcGllZH0gZmlsZShzKSwgc2tpcHBlZCAke3Jlc3VsdC5maWxlc1NraXBwZWR9LmA7CgogICAgbmV3IE5vdGljZShzdW1tYXJ5LCA4MDAwKTsKfQoKYXN5bmMgZnVuY3Rpb24gc2VsZWN0U291cmNlRm9sZGVyKGFwcCwgcXVpY2tBZGRBcGksIGRlZmF1bHRQYXRoLCBub3JtYWxpemVQYXRoKSB7CiAgICBjb25zdCBmb2xkZXJzID0gYXBwLnZhdWx0CiAgICAgICAgLmdldEFsbExvYWRlZEZpbGVzKCkKICAgICAgICAuZmlsdGVyKChmaWxlKSA9PiBmaWxlICYmIEFycmF5LmlzQXJyYXkoZmlsZS5jaGlsZHJlbikpCiAgICAgICAgLm1hcCgoZm9sZGVyKSA9PiBmb2xkZXIucGF0aCkKICAgICAgICAuZmlsdGVyKChwYXRoKSA9PiAhIXBhdGgpCiAgICAgICAgLnNvcnQoKGEsIGIpID0+IGEubG9jYWxlQ29tcGFyZShiKSk7CgogICAgaWYgKGZvbGRlcnMubGVuZ3RoID09PSAwKSB7CiAgICAgICAgcmV0dXJuIG51bGw7CiAgICB9CgogICAgY29uc3Qgbm9ybWFsaXplZERlZmF1bHQgPSBkZWZhdWx0UGF0aCA/IG5vcm1hbGl6ZVBhdGgoZGVmYXVsdFBhdGgudHJpbSgpKSA6ICIiOwogICAgY29uc3QgZGVmYXVsdEV4aXN0cyA9IG5vcm1hbGl6ZWREZWZhdWx0ICYmIGZvbGRlcnMuaW5jbHVkZXMobm9ybWFsaXplZERlZmF1bHQpOwoKICAgIGNvbnN0IHNvdXJjZSA9IGF3YWl0IHF1aWNrQWRkQXBpLnN1Z2dlc3RlcigKICAgICAgICBmb2xkZXJzLAogICAgICAgIGZvbGRlcnMsCiAgICAgICAgZGVmYXVsdEV4aXN0cwogICAgICAgICAgICA/IGBDaG9vc2Ugc291cmNlIGZvbGRlciAoZGVmYXVsdDogJHtub3JtYWxpemVkRGVmYXVsdH0pYAogICAgICAgICAgICA6ICJDaG9vc2Ugc291cmNlIGZvbGRlciIKICAgICk7CgogICAgcmV0dXJuIHNvdXJjZSA/IG5vcm1hbGl6ZVBhdGgoc291cmNlKSA6IG51bGw7Cn0KCmZ1bmN0aW9uIHNjYW5Gb2xkZXIoc291cmNlRm9sZGVyLCBURm9sZGVyLCBURmlsZSkgewogICAgY29uc3QgZm9sZGVycyA9IFtdOwogICAgY29uc3QgZmlsZXMgPSBbXTsKICAgIGNvbnN0IHF1ZXVlID0gWy4uLnNvdXJjZUZvbGRlci5jaGlsZHJlbl07CgogICAgd2hpbGUgKHF1ZXVlLmxlbmd0aCA+IDApIHsKICAgICAgICBjb25zdCBjdXJyZW50ID0gcXVldWUuc2hpZnQoKTsKICAgICAgICBpZiAoIWN1cnJlbnQpIGNvbnRpbnVlOwoKICAgICAgICBpZiAoY3VycmVudCBpbnN0YW5jZW9mIFRGb2xkZXIpIHsKICAgICAgICAgICAgZm9sZGVycy5wdXNoKGN1cnJlbnQpOwogICAgICAgICAgICBxdWV1ZS5wdXNoKC4uLmN1cnJlbnQuY2hpbGRyZW4pOwogICAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CgogICAgICAgIGlmIChjdXJyZW50IGluc3RhbmNlb2YgVEZpbGUpIHsKICAgICAgICAgICAgZmlsZXMucHVzaChjdXJyZW50KTsKICAgICAgICB9CiAgICB9CgogICAgZm9sZGVycy5zb3J0KChhLCBiKSA9PiBhLnBhdGgubG9jYWxlQ29tcGFyZShiLnBhdGgpKTsKICAgIGZpbGVzLnNvcnQoKGEsIGIpID0+IGEucGF0aC5sb2NhbGVDb21wYXJlKGIucGF0aCkpOwoKICAgIHJldHVybiB7IGZvbGRlcnMsIGZpbGVzIH07Cn0KCmZ1bmN0aW9uIGJ1aWxkUGxhbihzb3VyY2VSb290LCBkZXN0aW5hdGlvblJvb3QsIGZvbGRlcnMsIGZpbGVzLCBub3JtYWxpemVQYXRoKSB7CiAgICBjb25zdCBmb2xkZXJQYXRocyA9IFtkZXN0aW5hdGlvblJvb3RdOwogICAgY29uc3QgZmlsZVBsYW5zID0gW107CiAgICBjb25zdCBpbnZhbGlkUGF0aHMgPSBbXTsKCiAgICBmb3IgKGNvbnN0IGZvbGRlciBvZiBmb2xkZXJzKSB7CiAgICAgICAgY29uc3QgcmVsYXRpdmUgPSBmb2xkZXIucGF0aC5zbGljZShzb3VyY2VSb290Lmxlbmd0aCArIDEpOwogICAgICAgIGNvbnN0IHRhcmdldFBhdGggPSBub3JtYWxpemVQYXRoKGAke2Rlc3RpbmF0aW9uUm9vdH0vJHtyZWxhdGl2ZX1gKTsKCiAgICAgICAgaWYgKCFpc1N1YlBhdGgoZGVzdGluYXRpb25Sb290LCB0YXJnZXRQYXRoKSAmJiB0YXJnZXRQYXRoICE9PSBkZXN0aW5hdGlvblJvb3QpIHsKICAgICAgICAgICAgaW52YWxpZFBhdGhzLnB1c2godGFyZ2V0UGF0aCk7CiAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgIH0KCiAgICAgICAgZm9sZGVyUGF0aHMucHVzaCh0YXJnZXRQYXRoKTsKICAgIH0KCiAgICBmb3IgKGNvbnN0IGZpbGUgb2YgZmlsZXMpIHsKICAgICAgICBjb25zdCByZWxhdGl2ZSA9IGZpbGUucGF0aC5zbGljZShzb3VyY2VSb290Lmxlbmd0aCArIDEpOwogICAgICAgIGNvbnN0IHRhcmdldFBhdGggPSBub3JtYWxpemVQYXRoKGAke2Rlc3RpbmF0aW9uUm9vdH0vJHtyZWxhdGl2ZX1gKTsKCiAgICAgICAgaWYgKCFpc1N1YlBhdGgoZGVzdGluYXRpb25Sb290LCB0YXJnZXRQYXRoKSkgewogICAgICAgICAgICBpbnZhbGlkUGF0aHMucHVzaCh0YXJnZXRQYXRoKTsKICAgICAgICAgICAgY29udGludWU7CiAgICAgICAgfQoKICAgICAgICBmaWxlUGxhbnMucHVzaCh7IHNvdXJjZUZpbGU6IGZpbGUsIHRhcmdldFBhdGggfSk7CiAgICB9CgogICAgZm9sZGVyUGF0aHMuc29ydChzb3J0QnlEZXB0aFRoZW5QYXRoKTsKCiAgICByZXR1cm4geyBmb2xkZXJQYXRocywgZmlsZVBsYW5zLCBpbnZhbGlkUGF0aHMgfTsKfQoKYXN5bmMgZnVuY3Rpb24gZmluZEV4aXN0aW5nVGFyZ2V0cyhhcHAsIHBsYW4pIHsKICAgIGNvbnN0IGZvbGRlclBhdGhzID0gW107CiAgICBjb25zdCBmaWxlUGF0aHMgPSBbXTsKCiAgICBmb3IgKGNvbnN0IGZvbGRlclBhdGggb2YgcGxhbi5mb2xkZXJQYXRocykgewogICAgICAgIGlmIChhd2FpdCBhcHAudmF1bHQuYWRhcHRlci5leGlzdHMoZm9sZGVyUGF0aCkpIHsKICAgICAgICAgICAgZm9sZGVyUGF0aHMucHVzaChmb2xkZXJQYXRoKTsKICAgICAgICB9CiAgICB9CgogICAgZm9yIChjb25zdCBmaWxlUGxhbiBvZiBwbGFuLmZpbGVQbGFucykgewogICAgICAgIGlmIChhd2FpdCBhcHAudmF1bHQuYWRhcHRlci5leGlzdHMoZmlsZVBsYW4udGFyZ2V0UGF0aCkpIHsKICAgICAgICAgICAgZmlsZVBhdGhzLnB1c2goZmlsZVBsYW4udGFyZ2V0UGF0aCk7CiAgICAgICAgfQogICAgfQoKICAgIHJldHVybiB7IGZvbGRlclBhdGhzLCBmaWxlUGF0aHMgfTsKfQoKYXN5bmMgZnVuY3Rpb24gZXhlY3V0ZVBsYW4ob3B0aW9ucykgewogICAgY29uc3QgewogICAgICAgIGFwcCwKICAgICAgICBwbGFuLAogICAgICAgIGV4aXN0aW5nVGFyZ2V0cywKICAgICAgICBza2lwRXhpc3RpbmdGaWxlcywKICAgICAgICBzaG91bGRSZXBsYWNlSW5NYXJrZG93biwKICAgICAgICBmaW5kVGV4dCwKICAgICAgICByZXBsYWNlVGV4dCwKICAgICAgICBkcnlSdW4sCiAgICB9ID0gb3B0aW9uczsKCiAgICBjb25zdCBleGlzdGluZ0ZpbGVTZXQgPSBuZXcgU2V0KGV4aXN0aW5nVGFyZ2V0cy5maWxlUGF0aHMpOwogICAgbGV0IGZvbGRlcnNDcmVhdGVkID0gMDsKICAgIGxldCBmaWxlc0NvcGllZCA9IDA7CiAgICBsZXQgZmlsZXNTa2lwcGVkID0gMDsKICAgIGxldCBtYXJrZG93bkZpbGVzVXBkYXRlZCA9IDA7CgogICAgZm9yIChjb25zdCBmb2xkZXJQYXRoIG9mIHBsYW4uZm9sZGVyUGF0aHMpIHsKICAgICAgICBjb25zdCBhbHJlYWR5RXhpc3RzID0gYXdhaXQgYXBwLnZhdWx0LmFkYXB0ZXIuZXhpc3RzKGZvbGRlclBhdGgpOwogICAgICAgIGlmIChhbHJlYWR5RXhpc3RzKSBjb250aW51ZTsKCiAgICAgICAgaWYgKGRyeVJ1bikgewogICAgICAgICAgICBmb2xkZXJzQ3JlYXRlZCArPSAxOwogICAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CgogICAgICAgIGF3YWl0IGFwcC52YXVsdC5jcmVhdGVGb2xkZXIoZm9sZGVyUGF0aCk7CiAgICAgICAgZm9sZGVyc0NyZWF0ZWQgKz0gMTsKICAgIH0KCiAgICBmb3IgKGNvbnN0IGZpbGVQbGFuIG9mIHBsYW4uZmlsZVBsYW5zKSB7CiAgICAgICAgY29uc3QgeyBzb3VyY2VGaWxlLCB0YXJnZXRQYXRoIH0gPSBmaWxlUGxhbjsKICAgICAgICBjb25zdCB0YXJnZXRFeGlzdHMgPSBleGlzdGluZ0ZpbGVTZXQuaGFzKHRhcmdldFBhdGgpIHx8CiAgICAgICAgICAgIChhd2FpdCBhcHAudmF1bHQuYWRhcHRlci5leGlzdHModGFyZ2V0UGF0aCkpOwoKICAgICAgICBpZiAodGFyZ2V0RXhpc3RzKSB7CiAgICAgICAgICAgIGlmIChza2lwRXhpc3RpbmdGaWxlcykgewogICAgICAgICAgICAgICAgZmlsZXNTa2lwcGVkICs9IDE7CiAgICAgICAgICAgICAgICBjb250aW51ZTsKICAgICAgICAgICAgfQoKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBUYXJnZXQgZmlsZSBhbHJlYWR5IGV4aXN0czogJHt0YXJnZXRQYXRofWApOwogICAgICAgIH0KCiAgICAgICAgaWYgKGRyeVJ1bikgewogICAgICAgICAgICBmaWxlc0NvcGllZCArPSAxOwogICAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CgogICAgICAgIGlmIChzb3VyY2VGaWxlLmV4dGVuc2lvbi50b0xvd2VyQ2FzZSgpID09PSAibWQiKSB7CiAgICAgICAgICAgIGxldCBjb250ZW50ID0gYXdhaXQgYXBwLnZhdWx0LnJlYWQoc291cmNlRmlsZSk7CgogICAgICAgICAgICBpZiAoc2hvdWxkUmVwbGFjZUluTWFya2Rvd24gJiYgY29udGVudC5pbmNsdWRlcyhmaW5kVGV4dCkpIHsKICAgICAgICAgICAgICAgIGNvbnRlbnQgPSBjb250ZW50LnNwbGl0KGZpbmRUZXh0KS5qb2luKHJlcGxhY2VUZXh0KTsKICAgICAgICAgICAgICAgIG1hcmtkb3duRmlsZXNVcGRhdGVkICs9IDE7CiAgICAgICAgICAgIH0KCiAgICAgICAgICAgIGF3YWl0IGFwcC52YXVsdC5jcmVhdGUodGFyZ2V0UGF0aCwgY29udGVudCk7CiAgICAgICAgICAgIGZpbGVzQ29waWVkICs9IDE7CiAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgIH0KCiAgICAgICAgY29uc3QgYnl0ZXMgPSBhd2FpdCBhcHAudmF1bHQucmVhZEJpbmFyeShzb3VyY2VGaWxlKTsKICAgICAgICBhd2FpdCBhcHAudmF1bHQuY3JlYXRlQmluYXJ5KHRhcmdldFBhdGgsIGJ5dGVzKTsKICAgICAgICBmaWxlc0NvcGllZCArPSAxOwogICAgfQoKICAgIHJldHVybiB7CiAgICAgICAgZm9sZGVyc0NyZWF0ZWQsCiAgICAgICAgZmlsZXNDb3BpZWQsCiAgICAgICAgZmlsZXNTa2lwcGVkLAogICAgICAgIG1hcmtkb3duRmlsZXNVcGRhdGVkLAogICAgfTsKfQoKZnVuY3Rpb24gc29ydEJ5RGVwdGhUaGVuUGF0aChhLCBiKSB7CiAgICBjb25zdCBkZXB0aEEgPSBhLnNwbGl0KCIvIikubGVuZ3RoOwogICAgY29uc3QgZGVwdGhCID0gYi5zcGxpdCgiLyIpLmxlbmd0aDsKCiAgICBpZiAoZGVwdGhBICE9PSBkZXB0aEIpIHsKICAgICAgICByZXR1cm4gZGVwdGhBIC0gZGVwdGhCOwogICAgfQoKICAgIHJldHVybiBhLmxvY2FsZUNvbXBhcmUoYik7Cn0KCmZ1bmN0aW9uIGlzU3ViUGF0aChwYXJlbnRQYXRoLCBjaGlsZFBhdGgpIHsKICAgIGlmICghcGFyZW50UGF0aCB8fCAhY2hpbGRQYXRoKSByZXR1cm4gZmFsc2U7CiAgICByZXR1cm4gY2hpbGRQYXRoLnN0YXJ0c1dpdGgoYCR7cGFyZW50UGF0aH0vYCk7Cn0KCmZ1bmN0aW9uIGlzU2FmZVZhdWx0UGF0aChwYXRoKSB7CiAgICBpZiAoIXBhdGgpIHJldHVybiBmYWxzZTsKICAgIGlmIChwYXRoID09PSAiLiIgfHwgcGF0aCA9PT0gIi4uIikgcmV0dXJuIGZhbHNlOwogICAgaWYgKHBhdGguc3RhcnRzV2l0aCgiLi4vIikgfHwgcGF0aC5pbmNsdWRlcygiLy4uLyIpKSByZXR1cm4gZmFsc2U7CiAgICBpZiAocGF0aC5pbmNsdWRlcygiXFwiKSkgcmV0dXJuIGZhbHNlOwogICAgaWYgKHBhdGguc3RhcnRzV2l0aCgiLyIpKSByZXR1cm4gZmFsc2U7CiAgICByZXR1cm4gdHJ1ZTsKfQo=" | |
| } | |
| ] | |
| } |
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
| /** | |
| * Duplicate a folder (including subfolders and files) inside the current vault. | |
| * | |
| * Safe defaults: | |
| * - Never overwrites existing files. | |
| * - Blocks overlapping source/destination paths. | |
| * - Optional dry-run mode. | |
| * - Optional markdown text replacement for templating. | |
| */ | |
| module.exports = { | |
| entry: start, | |
| settings: { | |
| name: "Duplicate Folder", | |
| author: "QuickAdd", | |
| options: { | |
| "Default Source Folder": { | |
| type: "text", | |
| defaultValue: "", | |
| placeholder: "Projects/Template", | |
| description: "Optional default source folder to preselect" | |
| }, | |
| "Destination Suffix": { | |
| type: "text", | |
| defaultValue: " (Copy)", | |
| placeholder: " (Copy)", | |
| description: "Default suffix added to the suggested destination" | |
| }, | |
| "Allow Existing Destination Folder": { | |
| type: "toggle", | |
| defaultValue: false, | |
| description: "If disabled, abort when destination folder already exists" | |
| }, | |
| "Skip Existing Files": { | |
| type: "toggle", | |
| defaultValue: false, | |
| description: "If enabled, skip file collisions instead of aborting" | |
| }, | |
| "Find Text in Markdown": { | |
| type: "text", | |
| defaultValue: "", | |
| placeholder: "Old Project Name", | |
| description: "Optional plain text to replace in .md files" | |
| }, | |
| "Replace Text in Markdown": { | |
| type: "text", | |
| defaultValue: "", | |
| placeholder: "New Project Name", | |
| description: "Replacement text for markdown notes" | |
| }, | |
| "Dry Run": { | |
| type: "toggle", | |
| defaultValue: false, | |
| description: "Preview actions without writing files" | |
| } | |
| } | |
| } | |
| }; | |
| async function start(params, settings) { | |
| const { app, obsidian, quickAddApi, variables } = params; | |
| const { Notice, TFile, TFolder, normalizePath } = obsidian; | |
| const sourceFolderPath = await selectSourceFolder( | |
| app, | |
| quickAddApi, | |
| settings["Default Source Folder"], | |
| normalizePath | |
| ); | |
| if (!sourceFolderPath) { | |
| new Notice("Folder duplication cancelled."); | |
| return; | |
| } | |
| const sourceFolder = app.vault.getAbstractFileByPath(sourceFolderPath); | |
| if (!(sourceFolder instanceof TFolder)) { | |
| new Notice(`Source folder does not exist: ${sourceFolderPath}`); | |
| return; | |
| } | |
| const suggestedDestination = normalizePath( | |
| `${sourceFolderPath}${settings["Destination Suffix"] || " (Copy)"}` | |
| ); | |
| const destinationInput = await quickAddApi.inputPrompt( | |
| "Destination folder", | |
| "Where should the copy be created?", | |
| suggestedDestination | |
| ); | |
| if (!destinationInput || !destinationInput.trim()) { | |
| new Notice("Folder duplication cancelled."); | |
| return; | |
| } | |
| const destinationFolderPath = normalizePath(destinationInput.trim()); | |
| if (!isSafeVaultPath(destinationFolderPath)) { | |
| new Notice("Destination path is invalid."); | |
| return; | |
| } | |
| if (destinationFolderPath === sourceFolderPath) { | |
| new Notice("Destination cannot be the same as source."); | |
| return; | |
| } | |
| if ( | |
| isSubPath(sourceFolderPath, destinationFolderPath) || | |
| isSubPath(destinationFolderPath, sourceFolderPath) | |
| ) { | |
| new Notice( | |
| "Source and destination folders cannot overlap (parent/child)." | |
| ); | |
| return; | |
| } | |
| const allowExistingDestination = !!settings["Allow Existing Destination Folder"]; | |
| const skipExistingFiles = !!settings["Skip Existing Files"]; | |
| const dryRun = !!settings["Dry Run"]; | |
| const destinationExists = await app.vault.adapter.exists(destinationFolderPath); | |
| if (destinationExists && !allowExistingDestination) { | |
| new Notice( | |
| `Destination already exists: ${destinationFolderPath}. ` + | |
| "Enable 'Allow Existing Destination Folder' to continue." | |
| ); | |
| return; | |
| } | |
| const scan = scanFolder(sourceFolder, TFolder, TFile); | |
| const plan = buildPlan( | |
| sourceFolderPath, | |
| destinationFolderPath, | |
| scan.folders, | |
| scan.files, | |
| normalizePath | |
| ); | |
| if (plan.invalidPaths.length > 0) { | |
| new Notice( | |
| `Aborted due to invalid path mapping: ${plan.invalidPaths[0]}` | |
| ); | |
| return; | |
| } | |
| const existingTargets = await findExistingTargets(app, plan); | |
| if (existingTargets.filePaths.length > 0 && !skipExistingFiles) { | |
| new Notice( | |
| `Aborted: ${existingTargets.filePaths.length} file(s) already exist in destination.` | |
| ); | |
| return; | |
| } | |
| const findText = `${settings["Find Text in Markdown"] || ""}`; | |
| const replaceText = `${settings["Replace Text in Markdown"] || ""}`; | |
| const shouldReplaceInMarkdown = findText.length > 0; | |
| const previewLines = [ | |
| `Source: ${sourceFolderPath}`, | |
| `Destination: ${destinationFolderPath}`, | |
| `Folders to create: ${plan.folderPaths.length}`, | |
| `Files to copy: ${plan.filePlans.length}`, | |
| `Existing files: ${existingTargets.filePaths.length}`, | |
| `Dry run: ${dryRun ? "yes" : "no"}`, | |
| ]; | |
| if (shouldReplaceInMarkdown) { | |
| previewLines.push( | |
| `Markdown replacement: \"${findText}\" → \"${replaceText}\"` | |
| ); | |
| } | |
| const shouldContinue = await quickAddApi.yesNoPrompt( | |
| "Duplicate folder?", | |
| previewLines.join("\n") | |
| ); | |
| if (!shouldContinue) { | |
| new Notice("Folder duplication cancelled."); | |
| return; | |
| } | |
| let result; | |
| try { | |
| result = await executePlan({ | |
| app, | |
| plan, | |
| existingTargets, | |
| skipExistingFiles, | |
| shouldReplaceInMarkdown, | |
| findText, | |
| replaceText, | |
| dryRun, | |
| }); | |
| } catch (error) { | |
| const message = error?.message || `${error}`; | |
| new Notice(`Folder duplication failed: ${message}`, 8000); | |
| throw error; | |
| } | |
| variables.duplicatedSourceFolder = sourceFolderPath; | |
| variables.duplicatedDestinationFolder = destinationFolderPath; | |
| variables.duplicatedFoldersCreated = result.foldersCreated; | |
| variables.duplicatedFilesCopied = result.filesCopied; | |
| variables.duplicatedFilesSkipped = result.filesSkipped; | |
| variables.duplicatedMarkdownFilesUpdated = result.markdownFilesUpdated; | |
| const summary = dryRun | |
| ? `Dry run complete. Planned ${plan.folderPaths.length} folder(s) and ${plan.filePlans.length} file(s).` | |
| : `Duplicated folder successfully. Created ${result.foldersCreated} folder(s), copied ${result.filesCopied} file(s), skipped ${result.filesSkipped}.`; | |
| new Notice(summary, 8000); | |
| } | |
| async function selectSourceFolder(app, quickAddApi, defaultPath, normalizePath) { | |
| const folders = app.vault | |
| .getAllLoadedFiles() | |
| .filter((file) => file && Array.isArray(file.children)) | |
| .map((folder) => folder.path) | |
| .filter((path) => !!path) | |
| .sort((a, b) => a.localeCompare(b)); | |
| if (folders.length === 0) { | |
| return null; | |
| } | |
| const normalizedDefault = defaultPath ? normalizePath(defaultPath.trim()) : ""; | |
| const defaultExists = normalizedDefault && folders.includes(normalizedDefault); | |
| const source = await quickAddApi.suggester( | |
| folders, | |
| folders, | |
| defaultExists | |
| ? `Choose source folder (default: ${normalizedDefault})` | |
| : "Choose source folder" | |
| ); | |
| return source ? normalizePath(source) : null; | |
| } | |
| function scanFolder(sourceFolder, TFolder, TFile) { | |
| const folders = []; | |
| const files = []; | |
| const queue = [...sourceFolder.children]; | |
| while (queue.length > 0) { | |
| const current = queue.shift(); | |
| if (!current) continue; | |
| if (current instanceof TFolder) { | |
| folders.push(current); | |
| queue.push(...current.children); | |
| continue; | |
| } | |
| if (current instanceof TFile) { | |
| files.push(current); | |
| } | |
| } | |
| folders.sort((a, b) => a.path.localeCompare(b.path)); | |
| files.sort((a, b) => a.path.localeCompare(b.path)); | |
| return { folders, files }; | |
| } | |
| function buildPlan(sourceRoot, destinationRoot, folders, files, normalizePath) { | |
| const folderPaths = [destinationRoot]; | |
| const filePlans = []; | |
| const invalidPaths = []; | |
| for (const folder of folders) { | |
| const relative = folder.path.slice(sourceRoot.length + 1); | |
| const targetPath = normalizePath(`${destinationRoot}/${relative}`); | |
| if (!isSubPath(destinationRoot, targetPath) && targetPath !== destinationRoot) { | |
| invalidPaths.push(targetPath); | |
| continue; | |
| } | |
| folderPaths.push(targetPath); | |
| } | |
| for (const file of files) { | |
| const relative = file.path.slice(sourceRoot.length + 1); | |
| const targetPath = normalizePath(`${destinationRoot}/${relative}`); | |
| if (!isSubPath(destinationRoot, targetPath)) { | |
| invalidPaths.push(targetPath); | |
| continue; | |
| } | |
| filePlans.push({ sourceFile: file, targetPath }); | |
| } | |
| folderPaths.sort(sortByDepthThenPath); | |
| return { folderPaths, filePlans, invalidPaths }; | |
| } | |
| async function findExistingTargets(app, plan) { | |
| const folderPaths = []; | |
| const filePaths = []; | |
| for (const folderPath of plan.folderPaths) { | |
| if (await app.vault.adapter.exists(folderPath)) { | |
| folderPaths.push(folderPath); | |
| } | |
| } | |
| for (const filePlan of plan.filePlans) { | |
| if (await app.vault.adapter.exists(filePlan.targetPath)) { | |
| filePaths.push(filePlan.targetPath); | |
| } | |
| } | |
| return { folderPaths, filePaths }; | |
| } | |
| async function executePlan(options) { | |
| const { | |
| app, | |
| plan, | |
| existingTargets, | |
| skipExistingFiles, | |
| shouldReplaceInMarkdown, | |
| findText, | |
| replaceText, | |
| dryRun, | |
| } = options; | |
| const existingFileSet = new Set(existingTargets.filePaths); | |
| let foldersCreated = 0; | |
| let filesCopied = 0; | |
| let filesSkipped = 0; | |
| let markdownFilesUpdated = 0; | |
| for (const folderPath of plan.folderPaths) { | |
| const alreadyExists = await app.vault.adapter.exists(folderPath); | |
| if (alreadyExists) continue; | |
| if (dryRun) { | |
| foldersCreated += 1; | |
| continue; | |
| } | |
| await app.vault.createFolder(folderPath); | |
| foldersCreated += 1; | |
| } | |
| for (const filePlan of plan.filePlans) { | |
| const { sourceFile, targetPath } = filePlan; | |
| const targetExists = existingFileSet.has(targetPath) || | |
| (await app.vault.adapter.exists(targetPath)); | |
| if (targetExists) { | |
| if (skipExistingFiles) { | |
| filesSkipped += 1; | |
| continue; | |
| } | |
| throw new Error(`Target file already exists: ${targetPath}`); | |
| } | |
| if (dryRun) { | |
| filesCopied += 1; | |
| continue; | |
| } | |
| if (sourceFile.extension.toLowerCase() === "md") { | |
| let content = await app.vault.read(sourceFile); | |
| if (shouldReplaceInMarkdown && content.includes(findText)) { | |
| content = content.split(findText).join(replaceText); | |
| markdownFilesUpdated += 1; | |
| } | |
| await app.vault.create(targetPath, content); | |
| filesCopied += 1; | |
| continue; | |
| } | |
| const bytes = await app.vault.readBinary(sourceFile); | |
| await app.vault.createBinary(targetPath, bytes); | |
| filesCopied += 1; | |
| } | |
| return { | |
| foldersCreated, | |
| filesCopied, | |
| filesSkipped, | |
| markdownFilesUpdated, | |
| }; | |
| } | |
| function sortByDepthThenPath(a, b) { | |
| const depthA = a.split("/").length; | |
| const depthB = b.split("/").length; | |
| if (depthA !== depthB) { | |
| return depthA - depthB; | |
| } | |
| return a.localeCompare(b); | |
| } | |
| function isSubPath(parentPath, childPath) { | |
| if (!parentPath || !childPath) return false; | |
| return childPath.startsWith(`${parentPath}/`); | |
| } | |
| function isSafeVaultPath(path) { | |
| if (!path) return false; | |
| if (path === "." || path === "..") return false; | |
| if (path.startsWith("../") || path.includes("/../")) return false; | |
| if (path.includes("\\")) return false; | |
| if (path.startsWith("/")) return false; | |
| return true; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment