Skip to content

Commit 4d46dde

Browse files
authored
Merge pull request #3259 from ferdnyc/osp-file-repair
json_data: Autorepair corrupted project files
2 parents ee43136 + d259a22 commit 4d46dde

File tree

2 files changed

+105
-12
lines changed

2 files changed

+105
-12
lines changed

src/classes/json_data.py

+101-8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from classes.assets import get_assets_path
3737
from classes.logger import log
3838
from classes import info
39+
from classes.app import get_app
3940

4041
# Compiled path regex
4142
path_regex = re.compile(r'\"(image|path)\":.*?\"(.*?)\"')
@@ -53,6 +54,24 @@ def __init__(self):
5354
self._data = {} # Private data store, accessible through the get and set methods
5455
self.data_type = "json data"
5556

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+
5675
def get(self, key):
5776
""" Get copied value of a given key in data store """
5877
key = key.lower()
@@ -108,7 +127,7 @@ def merge_settings(self, default, user):
108127
# Update default values to match user values
109128
for item in default:
110129
user_value = user_values.get(item["setting"], None)
111-
if user_value != None:
130+
if user_value is not None:
112131
item["value"] = user_value
113132

114133
# Return merged list
@@ -129,16 +148,51 @@ def read_from_file(self, file_path, path_mode="ignore"):
129148
try:
130149
with open(file_path, 'r', encoding='utf-8') as f:
131150
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
137191
except Exception as ex:
138192
msg = ("Couldn't load {} file: {}".format(self.data_type, ex))
139193
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 = ()
142196
log.warning(msg)
143197
raise Exception(msg)
144198

@@ -261,3 +315,42 @@ def convert_paths_to_relative(self, file_path, previous_path, data):
261315
log.error("Error while converting absolute paths to relative paths: %s" % str(ex))
262316

263317
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

src/windows/main_window.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -532,10 +532,6 @@ def open_project(self, file_path, clear_thumbnails=True):
532532

533533
log.info("Loaded project {}".format(file_path))
534534
else:
535-
# Prepare to use status bar
536-
self.statusBar = QStatusBar()
537-
self.setStatusBar(self.statusBar)
538-
539535
log.info("File not found at {}".format(file_path))
540536
self.statusBar.showMessage(_("Project {} is missing (it may have been moved or deleted). It has been removed from the Recent Projects menu.".format(file_path)), 5000)
541537
self.remove_recent_project(file_path)
@@ -2553,6 +2549,10 @@ def __init__(self, mode=None):
25532549
self.effectsTreeView = EffectsListView(self)
25542550
self.tabEffects.layout().addWidget(self.effectsTreeView)
25552551

2552+
# Set up status bar
2553+
self.statusBar = QStatusBar()
2554+
self.setStatusBar(self.statusBar)
2555+
25562556
# Process events before continuing
25572557
# TODO: Figure out why this is needed for a backup recovery to correctly show up on the timeline
25582558
app.processEvents()

0 commit comments

Comments
 (0)