Create a gist now

Instantly share code, notes, and snippets.

@tonyseek /README.md
Last active Oct 28, 2017

What would you like to do?
Downgrade Sketch documents

Downgrade Sketch Document

Why

I don't wish to buy Sketch 3 again since I bought it from MacAppStore but lost continue upgrades and 5 devices support. A old version of Sketch will be restricted from opening documents created by the new versions of Sketch.

This is a workaround for some Sketch documents which don't depend on any new features. They could be opened by old version of Sketch if we modified their meta information by this script.

How

  1. Create a empty document by the current version of Sketch and inspect it: downgrade_sketch_document.py inspect --save ./Untitled.sketch
  2. Downgrade documents created by future versions of Sketch: downgrade_sketch_document.py downgrade ./Foo.sketch
#!/usr/bin/env python
from __future__ import absolute_import, print_function
import os
import sys
import json
import time
import zipfile
import shutil
import argparse
DOWNGRADE_FILEPATH = '~/.sketch-downgrade.json'
def load_downgrade_info():
downgrade_filepath = os.path.expanduser(DOWNGRADE_FILEPATH)
try:
with open(downgrade_filepath) as downgrade_file:
downgrade_info = json.load(downgrade_file)
except IOError:
downgrade_info = {}
downgrade_info.setdefault('app-version', '43.2')
downgrade_info.setdefault('build-code', '39069')
downgrade_info.setdefault('version-code', '88')
return downgrade_info
def load_file_info(path):
with zipfile.ZipFile(path) as sketch_file:
sketch_file.testzip()
meta = extract_meta(path, sketch_file)
return {
'app-version': meta['created']['appVersion'],
'build-code': meta['created']['build'],
'version-code': meta['created']['version']}
def change_file_info(path, info):
backup_path = '%s.%d.bak' % (path, int(time.time() * 1000))
target_path = '%s.%d.new' % (path, int(time.time() * 1000))
shutil.copyfile(path, backup_path)
with zipfile.ZipFile(backup_path) as origin_file, \
zipfile.ZipFile(target_path, 'w') as target_file:
origin_file.testzip()
meta = extract_meta(path, origin_file)
for item in meta, meta.setdefault('created', {}):
item['appVersion'] = info['app-version']
item['build'] = info['build-code']
item['version'] = info['version-code']
item['compatibilityVersion'] = info['version-code']
target_file.comment = origin_file.comment
for item in origin_file.infolist():
if item.filename == 'meta.json':
continue
target_file.writestr(item, origin_file.read(item.filename))
target_file.writestr('meta.json', json.dumps(meta))
shutil.move(target_path, path)
def extract_meta(path, sketch_file):
try:
with sketch_file.open('meta.json') as meta_file:
meta = json.load(meta_file)
except KeyError:
sketch_file.printdir()
print('-' * 79, file=sys.stderr)
print('Unrecognized sketch file: %s' % path, file=sys.stderr)
print('-' * 79, file=sys.stderr)
sys.exit(1)
return meta
def command_inspect(args):
file_info = load_file_info(args.file)
result = json.dumps(file_info, sort_keys=True, indent=2)
print(result)
if args.save:
downgrade_filepath = os.path.expanduser(DOWNGRADE_FILEPATH)
with open(downgrade_filepath, 'w') as downgrade_file:
downgrade_file.write(result)
downgrade_file.write('\n')
print('Saved to %s' % downgrade_filepath, file=sys.stderr)
def command_downgrade(args):
file_info = load_file_info(args.file)
downgrade_info = load_downgrade_info()
for key in ('app-version', 'build-code', 'version-code'):
print('%s\t%s (old)\t%s' % (key, file_info[key], downgrade_info[key]))
if file_info == downgrade_info:
sys.exit(0)
if not args.assumeyes:
answer = ''
while answer.lower() not in ('y', 'n'):
answer = raw_input('Continue? (Y/n) ')
if answer == 'n':
sys.exit(1)
change_file_info(args.file, downgrade_info)
def parse_args():
downgrade_info = load_downgrade_info()
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subparser')
parser_inspect = subparsers.add_parser('inspect')
parser_inspect.add_argument(
'-s', '--save', action='store_true', default=False,
help='save the inspect result to %s' % DOWNGRADE_FILEPATH)
parser_inspect.add_argument('file', metavar='FILE')
parser_downgrade = subparsers.add_parser('downgrade')
parser_downgrade.add_argument(
'-y', '--assumeyes', action='store_true', default=False,
help='answer yes to all questions')
parser_downgrade.add_argument(
'--app-version', default=downgrade_info['app-version'],
help='default: %(app-version)s' % downgrade_info)
parser_downgrade.add_argument(
'--build-code', default=downgrade_info['build-code'],
help='default: %(build-code)s' % downgrade_info)
parser_downgrade.add_argument(
'--version-code', default=downgrade_info['version-code'],
help='default: %(version-code)s' % downgrade_info)
parser_downgrade.add_argument('file', metavar='FILE')
return parser.parse_args()
def main():
args = parse_args()
command = {
'inspect': command_inspect,
'downgrade': command_downgrade,
}[args.subparser]
command(args)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment