36
36
from classes .assets import get_assets_path
37
37
from classes .logger import log
38
38
from classes import info
39
+ from classes .app import get_app
39
40
40
41
# Compiled path regex
41
42
path_regex = re .compile (r'\"(image|path)\":.*?\"(.*?)\"' )
@@ -53,6 +54,24 @@ def __init__(self):
53
54
self ._data = {} # Private data store, accessible through the get and set methods
54
55
self .data_type = "json data"
55
56
57
+ # Regular expression for project files with possible corruption
58
+ self .version_re = re .compile (r'"openshot-qt".*"2.5.0' )
59
+
60
+ # Regular expression matching likely corruption in project files
61
+ self .damage_re = re .compile (r'/u([0-9a-fA-F]{4})' )
62
+
63
+ # Connection to Qt main window, used in recovery alerts
64
+ app = get_app ()
65
+ if app :
66
+ self .app = app
67
+
68
+ if app and app ._tr :
69
+ self ._ = app ._tr
70
+ else :
71
+ def fn (arg ):
72
+ return arg
73
+ self ._ = fn
74
+
56
75
def get (self , key ):
57
76
""" Get copied value of a given key in data store """
58
77
key = key .lower ()
@@ -108,7 +127,7 @@ def merge_settings(self, default, user):
108
127
# Update default values to match user values
109
128
for item in default :
110
129
user_value = user_values .get (item ["setting" ], None )
111
- if user_value != None :
130
+ if user_value is not None :
112
131
item ["value" ] = user_value
113
132
114
133
# Return merged list
@@ -129,16 +148,51 @@ def read_from_file(self, file_path, path_mode="ignore"):
129
148
try :
130
149
with open (file_path , 'r' , encoding = 'utf-8' ) as f :
131
150
contents = f .read ()
132
- if contents :
133
- if path_mode == "absolute" :
134
- # Convert any paths to absolute
135
- contents = self .convert_paths_to_absolute (file_path , contents )
136
- return json .loads (contents )
151
+ if not contents :
152
+ raise RuntimeError ("Couldn't load {} file, no data." .format (self .data_type ))
153
+
154
+ # Scan for and correct possible OpenShot 2.5.0 corruption
155
+ if self .damage_re .search (contents ) and self .version_re .search (contents ):
156
+ # File contains corruptions, backup and repair
157
+ self .make_repair_backup (file_path , contents )
158
+
159
+ # Repair all corrupted escapes
160
+ contents , subs_count = self .damage_re .subn (r'\\u\1' , contents )
161
+
162
+ if subs_count < 1 :
163
+ # Nothing to do!
164
+ log .info ("No recovery substitutions on {}" .format (file_path ))
165
+ else :
166
+ # We have to de- and re-serialize the data, to complete repairs
167
+ temp_data = json .loads (contents )
168
+ contents = json .dumps (temp_data , ensure_ascii = False )
169
+ temp_data = {}
170
+
171
+ # Save the repaired data back to the original file
172
+ with open (file_path , "w" , encoding = "utf-8" ) as fout :
173
+ fout .write (contents )
174
+
175
+ msg_log = "Repaired {} corruptions in file {}"
176
+ msg_local = self ._ ("Repaired {num} corruptions in file {path}" )
177
+ log .info (msg_log .format (subs_count , file_path ))
178
+ if hasattr (self .app , "window" ) and hasattr (self .app .window , "statusBar" ):
179
+ self .app .window .statusBar .showMessage (
180
+ msg_local .format (num = subs_count , path = file_path ), 5000
181
+ )
182
+
183
+ # Process JSON data
184
+ if path_mode == "absolute" :
185
+ # Convert any paths to absolute
186
+ contents = self .convert_paths_to_absolute (file_path , contents )
187
+ return json .loads (contents )
188
+ except RuntimeError as ex :
189
+ log .error (str (ex ))
190
+ raise
137
191
except Exception as ex :
138
192
msg = ("Couldn't load {} file: {}" .format (self .data_type , ex ))
139
193
log .error (msg )
140
- raise Exception (msg )
141
- msg = ("Couldn't load {} file, no data." . format ( self . data_type ) )
194
+ raise Exception (msg ) from ex
195
+ msg = ()
142
196
log .warning (msg )
143
197
raise Exception (msg )
144
198
@@ -261,3 +315,42 @@ def convert_paths_to_relative(self, file_path, previous_path, data):
261
315
log .error ("Error while converting absolute paths to relative paths: %s" % str (ex ))
262
316
263
317
return data
318
+
319
+ def make_repair_backup (self , file_path , jsondata , backup_dir = None ):
320
+ """ Make a backup copy of an OSP file before performing recovery """
321
+
322
+ if backup_dir :
323
+ backup_base = os .path .join (backup_dir , os .path .basename (file_path ))
324
+ else :
325
+ backup_base = os .path .realpath (file_path )
326
+
327
+ # Generate a filename.osp.bak (or filename.osp.bak.1...) backup file name
328
+ backup_file = "{}.bak" .format (backup_base )
329
+ dup_count = 1
330
+ while os .path .exists (backup_file ) and dup_count <= 999 :
331
+ backup_file = "{}.bak.{}" .format (backup_base , dup_count )
332
+ dup_count += 1
333
+
334
+ if dup_count >= 1000 :
335
+ # Something's wrong, we can't find a free save file name; bail
336
+ raise Exception ("Aborting recovery, cannot create backup file" )
337
+
338
+ # Attempt to open backup file for writing and store original data
339
+ try :
340
+ with open (backup_file , "w" ) as fout :
341
+ fout .write (jsondata )
342
+
343
+ if hasattr (self .app , "window" ) and hasattr (self .app .window , "statusBar" ):
344
+ self .app .window .statusBar .showMessage (
345
+ self ._ ("Saved backup file {}" ).format (backup_file ), 5000
346
+ )
347
+ log .info ("Backed up {} as {}" .format (file_path , backup_file ))
348
+ except (PermissionError , FileExistsError ) as ex :
349
+ # Couldn't write to backup file! Try alternate location
350
+ log .error ("Couldn't write to backup file {}: {}" .format (backup_file , ex ))
351
+ if not backup_dir :
352
+ # Retry in alternate location
353
+ log .info ("Attempting to save backup in user config directory" )
354
+ self .make_repair_backup (file_path , jsondata , backup_dir = info .USER_PATH )
355
+ else :
356
+ raise Exception ("Aborting recovery, cannot write backup file" ) from ex
0 commit comments