Restoring backed up shortcuts using Scriptable

Updated Shortcut: https://routinehub.co/shortcut/613
Updated Script: https://raw.githubusercontent.com/supermamon/scriptable-scripts/master/Shortcuts%20Restore.js

I’m posting this in response to this request on this thread.

Background
I got a corrupted Shortcuts database a few days ago which prevented my shortcuts from showing on to the today widget. The prompt on the app said to send it to support or reset my database – which will delete all my shortcuts. I didn’t want to send it to support since there were some sensitive information in some of the shortcuts.

Force closing the Shortcut app and reopening gets me access on my shortcuts but still not on the today widget.
I couldn’t use @jim_sauer’s Backup & Restore because of the issue mentioned in the thread.

The Solution
I downloaded Scriptable a week prior but haven’t really gotten around using it. Probably because typing code on mobile is a pain. But due to the issue I had, I wondered if Scriptable can do file processing and just do a callback on Shortcuts to do the import. I am wishing that Scriptable will pause until I switch back to it after importing. It doesn’t matter if I have to manually switch back and forth manually to resume each import.

So after some research and experimentation, I got my first PoC. I set it to import only 2 files first and Lo and behold, I didn’t have manually switch back and forth. After some fine-tuning and a few pitfalls, I finally got ready.

It’s not a completely automated solution but it worked wonders! I hope this helps someone.

  1. Open the Files app. Create a Backups folder inside Shortcuts

  2. Use Shortcuts Backup to backup your shortcuts.

  3. Open the Files app again and move or copy the Backups folder into the Scriptable folder

  4. Run the Shortcuts Restore script to restore (code below)

     // Variables used by Scriptable.
     // These must be at the very top of the file. Do not edit.
     // icon-color: red; icon-glyph: undo;
     /*
     Script  : Restore Shortcuts
     Author  : @supermamon
     Version : 1.0.0
     Changes :
         v1.0.0 | 2018-09-29
         - Initial release     
    
     Assumptions:
     - .shortcut files are located in the BACKUP_LOCATION directory
     - there is an Info.json file in the BACKUP_LOCATION directory in this format:
    
         {
             "Tier1": [ ... ],
             "ListOrder" : [ ... ]
         }
    
     - Tier1: list of shortcut names that are imported first
     - ListOrder: the remaing shortcuts
     - You can use this Shortcut to create your backup 
         > https://www.icloud.com/shortcuts/7f65c2d812e64ce68190c2c1d7a43ad1
    
     Shortcomings:
     - It failed to restore the shortcuts where the name contains emojis.
    
     */
    
     const FM    = FileManager.iCloud();
     const HOME  = FM.documentsDirectory();
    
     // Configure
     // these are relative to the scriptable home directory
     const BACKUP_LOCATION   = 'Backups/'  
     const BACKUP_INFO_FILE  = 'Backups/Info.json'
    
     // set DEBUG = false to proceed with full import.
     // in DEBUG mode,
     //    restored shortcuts will be appended with ' (restored)' in their names
     //    will only restore 2 from Tier1 and 2 from ListOrder. Adjust below if needed  
     const DEBUG = true
     const DEBUG_TEST_IMPORT_QTY = 2 
    
     // Base64 helper
     // found it here: https://talk.automators.fm/t/is-it-possible-to-base64-encode-a-string/1547/7
     //      which links to here: https://scotch.io/tutorials/how-to-encode-and-decode-strings-with-base64-in-javascript
     var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(e){var t="";var n,r,i,s,o,u,a;var f=0;e=Base64._utf8_encode(e);while(f<e.length){n=e.charCodeAt(f++);r=e.charCodeAt(f++);i=e.charCodeAt(f++);s=n>>2;o=(n&3)<<4|r>>4;u=(r&15)<<2|i>>6;a=i&63;if(isNaN(r)){u=a=64}else if(isNaN(i)){a=64}t=t+this._keyStr.charAt(s)+this._keyStr.charAt(o)+this._keyStr.charAt(u)+this._keyStr.charAt(a)}return t},decode:function(e){var t="";var n,r,i;var s,o,u,a;var f=0;e=e.replace(/[^A-Za-z0-9+/=]/g,"");while(f<e.length){s=this._keyStr.indexOf(e.charAt(f++));o=this._keyStr.indexOf(e.charAt(f++));u=this._keyStr.indexOf(e.charAt(f++));a=this._keyStr.indexOf(e.charAt(f++));n=s<<2|o>>4;r=(o&15)<<4|u>>2;i=(u&3)<<6|a;t=t+String.fromCharCode(n);if(u!=64){t=t+String.fromCharCode(r)}if(a!=64){t=t+String.fromCharCode(i)}}t=Base64._utf8_decode(t);return t},_utf8_encode:function(e){e=e.replace(/\r\n/g,"\n");var t="";for(var n=0;n<e.length;n++){var r=e.charCodeAt(n);if(r<128){t+=String.fromCharCode(r)}else if(r>127&&r<2048){t+=String.fromCharCode(r>>6|192);t+=String.fromCharCode(r&63|128)}else{t+=String.fromCharCode(r>>12|224);t+=String.fromCharCode(r>>6&63|128);t+=String.fromCharCode(r&63|128)}}return t},_utf8_decode:function(e){var t="";var n=0;var r=c1=c2=0;while(n<e.length){r=e.charCodeAt(n);if(r<128){t+=String.fromCharCode(r);n++}else if(r>191&&r<224){c2=e.charCodeAt(n+1);t+=String.fromCharCode((r&31)<<6|c2&63);n+=2}else{c2=e.charCodeAt(n+1);c3=e.charCodeAt(n+2);t+=String.fromCharCode((r&15)<<12|(c2&63)<<6|c3&63);n+=3}}return t}}
    
     function importShortcut(shortcutPath) {
         // expects an absolute path
         if (!FM.fileExists(shortcutPath)) return;
    
         const scName      = FM.fileName(shortcutPath,false);
    
         console.log(`importing > ${scName}`)
    
         // read the file and convert it into an encoded URL
         let d             = Data.fromFile(shortcutPath);
         let file          = d.toBase64String();
         let durl          = "data:text/shortcut;base64," + file;
         let encodedUri    = encodeURI(durl);
         console.log(`          > ${scName} loaded`)
    
         // callback to import it to the Shortcuts app
         const baseURL     = "shortcuts://import-shortcut";
         const url         = new CallbackURL(baseURL);
         url.addParameter("name",scName+(DEBUG?' (restored)':''));
         url.addParameter("url",encodedUri); 
         url.open();
         console.log(`          > ${scName} sent for import`)
    
     }
    
     function loadJSON(relPath) {
         const apath       = FM.joinPath(HOME, relPath);
         const FMl         = FileManager.local();
         const contents    = Base64.decode(FMl.read(apath).toBase64String());
         return JSON.parse(contents)
     }
    
    
     function main() {
         const backupInfo = loadJSON(BACKUP_INFO_FILE);
    
         // generate absolute paths
         const t1 = backupInfo.Tier1.map( file => { 
             return FM.joinPath(HOME,`${BACKUP_LOCATION}${file}.shortcut`)
         })
         const t2 = backupInfo.ListOrder.map( file => { 
             return FM.joinPath(HOME,`${BACKUP_LOCATION}${file}.shortcut`)
         })
    
         console.log('Importing Tier 1 shortcuts')
         for (var i=0; i<(DEBUG?DEBUG_TEST_IMPORT_QTY:t1.length); i++) {
             console.log(i)
             importShortcut(t1[i])
         }
    
         console.log('Importing the remaining shortcuts')  
         for (var i=0; i<(DEBUG?DEBUG_TEST_IMPORT_QTY:t2.length); i++) {
             console.log(i)
             importShortcut(t2[i])
         }
     }
    
     main()
4 Likes