mirror of
https://github.com/netinvent/npbackup.git
synced 2024-09-20 06:46:13 +08:00
GUI: Highly improve restore window memory usage
This commit is contained in:
parent
65d4559bbd
commit
410121e2cf
|
@ -7,7 +7,7 @@ __intname__ = "npbackup.gui.core.runner"
|
||||||
__author__ = "Orsiris de Jong"
|
__author__ = "Orsiris de Jong"
|
||||||
__copyright__ = "Copyright (C) 2022-2024 NetInvent"
|
__copyright__ = "Copyright (C) 2022-2024 NetInvent"
|
||||||
__license__ = "GPL-3.0-only"
|
__license__ = "GPL-3.0-only"
|
||||||
__build__ = "2024061101"
|
__build__ = "2024091501"
|
||||||
|
|
||||||
|
|
||||||
from typing import Optional, Callable, Union, List
|
from typing import Optional, Callable, Union, List
|
||||||
|
@ -220,6 +220,9 @@ class NPBackupRunner:
|
||||||
self._verbose = False
|
self._verbose = False
|
||||||
self._live_output = False
|
self._live_output = False
|
||||||
self._json_output = False
|
self._json_output = False
|
||||||
|
# struct_output is msgspec.Struct instead of json, which is less memory consuming
|
||||||
|
# struct_output neeeds json_output to be True
|
||||||
|
self._struct_output = False
|
||||||
self._binary = None
|
self._binary = None
|
||||||
self._no_cache = False
|
self._no_cache = False
|
||||||
self.restic_runner = None
|
self.restic_runner = None
|
||||||
|
@ -300,6 +303,17 @@ class NPBackupRunner:
|
||||||
self.write_logs(msg, level="critical", raise_error="ValueError")
|
self.write_logs(msg, level="critical", raise_error="ValueError")
|
||||||
self._json_output = value
|
self._json_output = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def struct_output(self):
|
||||||
|
return self._struct_output
|
||||||
|
|
||||||
|
@struct_output.setter
|
||||||
|
def struct_output(self, value):
|
||||||
|
if not isinstance(value, bool):
|
||||||
|
msg = f"Bogus struct_output parameter given: {value}"
|
||||||
|
self.write_logs(msg, level="critical", raise_error="ValueError")
|
||||||
|
self._struct_output = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stdout(self):
|
def stdout(self):
|
||||||
return self._stdout
|
return self._stdout
|
||||||
|
@ -409,7 +423,7 @@ class NPBackupRunner:
|
||||||
f"Runner took {self.exec_time} seconds for {fn.__name__}", level="info"
|
f"Runner took {self.exec_time} seconds for {fn.__name__}", level="info"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
os.environ['NPBACKUP_EXEC_TIME'] = str(self.exec_time)
|
os.environ["NPBACKUP_EXEC_TIME"] = str(self.exec_time)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
return result
|
return result
|
||||||
|
@ -826,6 +840,7 @@ class NPBackupRunner:
|
||||||
self.restic_runner.no_cache = self.no_cache
|
self.restic_runner.no_cache = self.no_cache
|
||||||
self.restic_runner.live_output = self.live_output
|
self.restic_runner.live_output = self.live_output
|
||||||
self.restic_runner.json_output = self.json_output
|
self.restic_runner.json_output = self.json_output
|
||||||
|
self.restic_runner.struct_output = self.struct_output
|
||||||
self.restic_runner.stdout = self.stdout
|
self.restic_runner.stdout = self.stdout
|
||||||
self.restic_runner.stderr = self.stderr
|
self.restic_runner.stderr = self.stderr
|
||||||
if self.binary:
|
if self.binary:
|
||||||
|
|
|
@ -13,6 +13,7 @@ from typing import List, Optional, Tuple
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import gc
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
@ -41,6 +42,8 @@ from resources.customization import (
|
||||||
LOADER_ANIMATION,
|
LOADER_ANIMATION,
|
||||||
FOLDER_ICON,
|
FOLDER_ICON,
|
||||||
FILE_ICON,
|
FILE_ICON,
|
||||||
|
SYMLINK_ICON,
|
||||||
|
IRREGULAR_FILE_ICON,
|
||||||
LICENSE_TEXT,
|
LICENSE_TEXT,
|
||||||
SIMPLEGUI_THEME,
|
SIMPLEGUI_THEME,
|
||||||
OEM_ICON,
|
OEM_ICON,
|
||||||
|
@ -55,6 +58,7 @@ from npbackup.path_helper import CURRENT_DIR
|
||||||
from npbackup.__version__ import version_string
|
from npbackup.__version__ import version_string
|
||||||
from npbackup.__debug__ import _DEBUG
|
from npbackup.__debug__ import _DEBUG
|
||||||
from npbackup.restic_wrapper import ResticRunner
|
from npbackup.restic_wrapper import ResticRunner
|
||||||
|
from npbackup.restic_wrapper import schema
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger()
|
logger = getLogger()
|
||||||
|
@ -183,39 +187,60 @@ def _make_treedata_from_json(ls_result: List[dict]) -> sg.TreeData:
|
||||||
{'name': 'unsupported.tcl', 'type': 'file', 'path': '/C/GIT/npbackup/npbackup.dist/tk/unsupported.tcl', 'uid': 0, 'gid': 0, 'size': 10521, 'mode': 438, 'permissions': '-rw-rw-rw-', 'mtime': '2022-09-05T14:18:52+02:00', 'atime': '2022-09-05T14:18:52+02:00', 'ctime': '2022-09-05T14:18:52+02:00', 'struct_type': 'node'}
|
{'name': 'unsupported.tcl', 'type': 'file', 'path': '/C/GIT/npbackup/npbackup.dist/tk/unsupported.tcl', 'uid': 0, 'gid': 0, 'size': 10521, 'mode': 438, 'permissions': '-rw-rw-rw-', 'mtime': '2022-09-05T14:18:52+02:00', 'atime': '2022-09-05T14:18:52+02:00', 'ctime': '2022-09-05T14:18:52+02:00', 'struct_type': 'node'}
|
||||||
{'name': 'xmfbox.tcl', 'type': 'file', 'path': '/C/GIT/npbackup/npbackup.dist/tk/xmfbox.tcl', 'uid': 0, 'gid': 0, 'size': 27064, 'mode': 438, 'permissions': '-rw-rw-rw-', 'mtime': '2022-09-05T14:18:52+02:00', 'atime': '2022-09-05T14:18:52+02:00', 'ctime': '2022-09-05T14:18:52+02:00', 'struct_type': 'node'}
|
{'name': 'xmfbox.tcl', 'type': 'file', 'path': '/C/GIT/npbackup/npbackup.dist/tk/xmfbox.tcl', 'uid': 0, 'gid': 0, 'size': 27064, 'mode': 438, 'permissions': '-rw-rw-rw-', 'mtime': '2022-09-05T14:18:52+02:00', 'atime': '2022-09-05T14:18:52+02:00', 'ctime': '2022-09-05T14:18:52+02:00', 'struct_type': 'node'}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Since v3-rc6, we're actually using a msgspec.Struct represenation which uses dot notation
|
||||||
"""
|
"""
|
||||||
treedata = sg.TreeData()
|
treedata = sg.TreeData()
|
||||||
count = 0
|
count = 0
|
||||||
for entry in ls_result:
|
for entry in ls_result:
|
||||||
# Make sure we drop the prefix '/' so sg.TreeData does not get an empty root
|
# Make sure we drop the prefix '/' so sg.TreeData does not get an empty root
|
||||||
entry["path"] = entry["path"].lstrip("/")
|
|
||||||
|
entry.path = entry.path.lstrip("/")
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
# On windows, we need to make sure tree keys don't get duplicate because of lower/uppercase
|
# On windows, we need to make sure tree keys don't get duplicate because of lower/uppercase
|
||||||
# Shown filenames aren't affected by this
|
# Shown filenames aren't affected by this
|
||||||
entry["path"] = entry["path"].lower()
|
entry.path = entry.path.lower()
|
||||||
parent = os.path.dirname(entry["path"])
|
parent = os.path.dirname(entry.path)
|
||||||
|
|
||||||
# Make sure we normalize mtime, and remove microseconds
|
# Make sure we normalize mtime, and remove microseconds
|
||||||
# dateutil.parser.parse is *really* cpu hungry, let's replace it with a dumb alternative
|
# dateutil.parser.parse is *really* cpu hungry, let's replace it with a dumb alternative
|
||||||
# mtime = dateutil.parser.parse(entry["mtime"]).strftime("%Y-%m-%d %H:%M:%S")
|
# mtime = dateutil.parser.parse(entry["mtime"]).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
mtime = str(entry["mtime"])[0:19]
|
mtime = entry.mtime.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
if entry["type"] == "dir" and entry["path"] not in treedata.tree_dict:
|
name = os.path.basename(entry.path)
|
||||||
|
if entry.type == schema.LsNodeType.DIR and entry.path not in treedata.tree_dict:
|
||||||
treedata.Insert(
|
treedata.Insert(
|
||||||
parent=parent,
|
parent=parent,
|
||||||
key=entry["path"],
|
key=entry.path,
|
||||||
text=entry["name"],
|
text=name,
|
||||||
values=["", mtime],
|
values=["", mtime],
|
||||||
icon=FOLDER_ICON,
|
icon=FOLDER_ICON,
|
||||||
)
|
)
|
||||||
elif entry["type"] == "file":
|
elif entry.type == schema.LsNodeType.FILE:
|
||||||
size = BytesConverter(entry["size"]).human
|
size = BytesConverter(entry.size).human
|
||||||
treedata.Insert(
|
treedata.Insert(
|
||||||
parent=parent,
|
parent=parent,
|
||||||
key=entry["path"],
|
key=entry.path,
|
||||||
text=entry["name"],
|
text=name,
|
||||||
values=[size, mtime],
|
values=[size, mtime],
|
||||||
icon=FILE_ICON,
|
icon=FILE_ICON,
|
||||||
)
|
)
|
||||||
|
elif entry.type == schema.LsNodeType.SYMLINK:
|
||||||
|
treedata.Insert(
|
||||||
|
parent=parent,
|
||||||
|
key=entry.path,
|
||||||
|
text=name,
|
||||||
|
values=["", mtime],
|
||||||
|
icon=SYMLINK_ICON,
|
||||||
|
)
|
||||||
|
elif entry.type == schema.LsNodeType.IRREGULAR:
|
||||||
|
treedata.Insert(
|
||||||
|
parent=parent,
|
||||||
|
key=entry.path,
|
||||||
|
text=name,
|
||||||
|
values=["", mtime],
|
||||||
|
icon=IRREGULAR_FILE_ICON,
|
||||||
|
)
|
||||||
|
|
||||||
# Since the thread is heavily CPU bound, let's add a minimal
|
# Since the thread is heavily CPU bound, let's add a minimal
|
||||||
# arbitrary sleep time to let GUI update
|
# arbitrary sleep time to let GUI update
|
||||||
# In a 130k entry scenario, using count % 1000 added less than a second on a 25 second run
|
# In a 130k entry scenario, using count % 1000 added less than a second on a 25 second run
|
||||||
|
@ -237,12 +262,13 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
||||||
__backend_binary=backend_binary,
|
__backend_binary=backend_binary,
|
||||||
)
|
)
|
||||||
if not result or not result["result"]:
|
if not result or not result["result"]:
|
||||||
sg.Popup("main_gui.snapshot_is_empty")
|
sg.Popup(_t("main_gui.snapshot_is_empty"))
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# result is {"result": True, "output": [{snapshot_description}, {entry}, {entry}]}
|
# result is {"result": True, "output": [{snapshot_description}, {entry}, {entry}]}
|
||||||
content = result["output"]
|
# content = result["output"]
|
||||||
# First entry of snapshot list is the snapshot description
|
# First entry of snapshot list is the snapshot description
|
||||||
snapshot = content.pop(0)
|
snapshot = result["output"].pop(0)
|
||||||
try:
|
try:
|
||||||
snap_date = dateutil.parser.parse(snapshot["time"])
|
snap_date = dateutil.parser.parse(snapshot["time"])
|
||||||
except (KeyError, IndexError, TypeError):
|
except (KeyError, IndexError, TypeError):
|
||||||
|
@ -261,7 +287,6 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
||||||
hostname = "[inconnu]"
|
hostname = "[inconnu]"
|
||||||
|
|
||||||
backup_id = f"{_t('main_gui.backup_content_from')} {snap_date} {_t('main_gui.run_as')} {username}@{hostname} {_t('main_gui.identified_by')} {short_id}"
|
backup_id = f"{_t('main_gui.backup_content_from')} {snap_date} {_t('main_gui.run_as')} {username}@{hostname} {_t('main_gui.identified_by')} {short_id}"
|
||||||
|
|
||||||
if not backup_id or not snapshot or not short_id:
|
if not backup_id or not snapshot or not short_id:
|
||||||
sg.PopupError(_t("main_gui.cannot_get_content"), keep_on_top=True)
|
sg.PopupError(_t("main_gui.cannot_get_content"), keep_on_top=True)
|
||||||
return False
|
return False
|
||||||
|
@ -272,7 +297,8 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
||||||
|
|
||||||
# We get a thread result, hence pylint will complain the thread isn't a tuple
|
# We get a thread result, hence pylint will complain the thread isn't a tuple
|
||||||
# pylint: disable=E1101 (no-member)
|
# pylint: disable=E1101 (no-member)
|
||||||
thread = _make_treedata_from_json(content)
|
|
||||||
|
thread = _make_treedata_from_json(result["output"])
|
||||||
while not thread.done() and not thread.cancelled():
|
while not thread.done() and not thread.cancelled():
|
||||||
sg.PopupAnimated(
|
sg.PopupAnimated(
|
||||||
LOADER_ANIMATION,
|
LOADER_ANIMATION,
|
||||||
|
@ -283,6 +309,8 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
||||||
)
|
)
|
||||||
sg.PopupAnimated(None)
|
sg.PopupAnimated(None)
|
||||||
|
|
||||||
|
logger.info("Finished creating data tree")
|
||||||
|
|
||||||
left_col = [
|
left_col = [
|
||||||
[sg.Text(backup_id)],
|
[sg.Text(backup_id)],
|
||||||
[
|
[
|
||||||
|
@ -316,8 +344,11 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
||||||
enable_close_attempted_event=True,
|
enable_close_attempted_event=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reclaim memory fro thread result
|
# Reclaim memory from thread result
|
||||||
thread = None
|
# Note from v3 dev: This doesn't actually improve memory usage
|
||||||
|
del thread
|
||||||
|
del result
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event, values = window.read()
|
event, values = window.read()
|
||||||
|
@ -334,6 +365,7 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
||||||
# before closing the window
|
# before closing the window
|
||||||
window["-TREE-"].update(values=sg.TreeData())
|
window["-TREE-"].update(values=sg.TreeData())
|
||||||
window.close()
|
window.close()
|
||||||
|
del window
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -966,6 +998,7 @@ def _main_gui(viewer_mode: bool):
|
||||||
continue
|
continue
|
||||||
snapshot_to_see = snapshot_list[values["snapshot-list"][0]][0]
|
snapshot_to_see = snapshot_list[values["snapshot-list"][0]][0]
|
||||||
ls_window(repo_config, snapshot_to_see)
|
ls_window(repo_config, snapshot_to_see)
|
||||||
|
gc.collect()
|
||||||
if event == "--FORGET--":
|
if event == "--FORGET--":
|
||||||
if not full_config:
|
if not full_config:
|
||||||
sg.PopupError(_t("main_gui.no_config"), keep_on_top=True)
|
sg.PopupError(_t("main_gui.no_config"), keep_on_top=True)
|
||||||
|
|
|
@ -127,6 +127,10 @@ def gui_thread_runner(
|
||||||
|
|
||||||
# We'll always use json output in GUI mode
|
# We'll always use json output in GUI mode
|
||||||
runner.json_output = True
|
runner.json_output = True
|
||||||
|
# in GUI mode, we'll use struct output instead of json whenever it's possible
|
||||||
|
# as of v3, this only is needed for ls operations
|
||||||
|
runner.struct_output = True
|
||||||
|
|
||||||
# So we don't always init repo_config, since runner.group_runner would do that itself
|
# So we don't always init repo_config, since runner.group_runner would do that itself
|
||||||
if __repo_config:
|
if __repo_config:
|
||||||
runner.repo_config = __repo_config
|
runner.repo_config = __repo_config
|
||||||
|
|
|
@ -7,8 +7,8 @@ __intname__ = "npbackup.restic_wrapper"
|
||||||
__author__ = "Orsiris de Jong"
|
__author__ = "Orsiris de Jong"
|
||||||
__copyright__ = "Copyright (C) 2022-2024 NetInvent"
|
__copyright__ = "Copyright (C) 2022-2024 NetInvent"
|
||||||
__license__ = "GPL-3.0-only"
|
__license__ = "GPL-3.0-only"
|
||||||
__build__ = "2024090602"
|
__build__ = "2024091501"
|
||||||
__version__ = "2.2.4"
|
__version__ = "2.3.0"
|
||||||
|
|
||||||
|
|
||||||
from typing import Tuple, List, Optional, Callable, Union
|
from typing import Tuple, List, Optional, Callable, Union
|
||||||
|
@ -16,7 +16,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import re
|
import re
|
||||||
import json
|
import msgspec
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
import queue
|
import queue
|
||||||
|
@ -26,7 +26,7 @@ from ofunctions.misc import BytesConverter, fn_name
|
||||||
from npbackup.__debug__ import _DEBUG
|
from npbackup.__debug__ import _DEBUG
|
||||||
from npbackup.__env__ import FAST_COMMANDS_TIMEOUT, CHECK_INTERVAL
|
from npbackup.__env__ import FAST_COMMANDS_TIMEOUT, CHECK_INTERVAL
|
||||||
from npbackup.path_helper import CURRENT_DIR
|
from npbackup.path_helper import CURRENT_DIR
|
||||||
|
from npbackup.restic_wrapper import schema
|
||||||
|
|
||||||
logger = getLogger()
|
logger = getLogger()
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ class ResticRunner:
|
||||||
self._dry_run = False
|
self._dry_run = False
|
||||||
self._no_cache = False
|
self._no_cache = False
|
||||||
self._json_output = False
|
self._json_output = False
|
||||||
|
self._struct_output = False
|
||||||
|
|
||||||
self.backup_result_content = None
|
self.backup_result_content = None
|
||||||
|
|
||||||
|
@ -222,6 +223,17 @@ class ResticRunner:
|
||||||
else:
|
else:
|
||||||
raise ValueError("Bogus json_output value given")
|
raise ValueError("Bogus json_output value given")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def struct_output(self) -> bool:
|
||||||
|
return self._struct_output
|
||||||
|
|
||||||
|
@struct_output.setter
|
||||||
|
def struct_output(self, value: bool):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
self._struct_output = value
|
||||||
|
else:
|
||||||
|
raise ValueError("Bogus struct_output value given")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ignore_cloud_files(self) -> bool:
|
def ignore_cloud_files(self) -> bool:
|
||||||
return self._ignore_cloud_files
|
return self._ignore_cloud_files
|
||||||
|
@ -636,6 +648,10 @@ class ResticRunner:
|
||||||
return self.convert_to_json_output(False, output=msg)
|
return self.convert_to_json_output(False, output=msg)
|
||||||
else:
|
else:
|
||||||
# pylint: disable=E1101 (no-member)
|
# pylint: disable=E1101 (no-member)
|
||||||
|
output = output.replace(
|
||||||
|
self.repository,
|
||||||
|
self.repository.split(":")[0] + ":_(o_O)_hidden_by_npbackup",
|
||||||
|
)
|
||||||
msg = f"Backend is not ready to perform operation {fn.__name__}. Repo maybe inaccessible or not initialized. You can try to run a backup to initialize the repository:\n{output}." # pylint: disable=E1101 (no-member)
|
msg = f"Backend is not ready to perform operation {fn.__name__}. Repo maybe inaccessible or not initialized. You can try to run a backup to initialize the repository:\n{output}." # pylint: disable=E1101 (no-member)
|
||||||
return self.convert_to_json_output(False, msg=msg)
|
return self.convert_to_json_output(False, msg=msg)
|
||||||
# pylint: disable=E1102 (not-callable)
|
# pylint: disable=E1102 (not-callable)
|
||||||
|
@ -658,31 +674,40 @@ class ResticRunner:
|
||||||
js = {
|
js = {
|
||||||
"result": result,
|
"result": result,
|
||||||
"operation": operation,
|
"operation": operation,
|
||||||
|
"extended_info": None,
|
||||||
"args": kwargs,
|
"args": kwargs,
|
||||||
"output": None,
|
"output": [],
|
||||||
}
|
}
|
||||||
if result:
|
if result:
|
||||||
if output:
|
if output:
|
||||||
if isinstance(output, str):
|
decoder = msgspec.json.Decoder()
|
||||||
output = list(filter(None, output.split("\n")))
|
ls_decoder = msgspec.json.Decoder(schema.LsNode)
|
||||||
else:
|
is_first_line = True
|
||||||
output = [str(output)]
|
|
||||||
if len(output) > 1:
|
for line in output.split("\n"):
|
||||||
output_is_list = True
|
if not line:
|
||||||
js["output"] = []
|
continue
|
||||||
else:
|
try:
|
||||||
output_is_list = False
|
if (
|
||||||
for line in output:
|
not is_first_line
|
||||||
if output_is_list:
|
and operation == "ls"
|
||||||
try:
|
and self.struct_output
|
||||||
js["output"].append(json.loads(line))
|
):
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
js["output"].append(line)
|
js["output"].append(ls_decoder.decode(line))
|
||||||
else:
|
else:
|
||||||
try:
|
js["output"].append(decoder.decode(line))
|
||||||
js["output"] = json.loads(line)
|
is_first_line = False
|
||||||
except json.decoder.JSONDecodeError:
|
except msgspec.DecodeError as exc:
|
||||||
js["output"] = {"data": line}
|
msg = f"JSON decode error: {exc} on content '{line}'"
|
||||||
|
self.write_logs(msg, level="error")
|
||||||
|
js["extended_info"] = msg
|
||||||
|
js["output"].append({"data": line})
|
||||||
|
js["result"] = False
|
||||||
|
# If we only have one output, we don't need a list
|
||||||
|
if len(js["output"]) == 1:
|
||||||
|
js["output"] = js["output"][0]
|
||||||
|
|
||||||
if msg:
|
if msg:
|
||||||
self.write_logs(msg, level="info")
|
self.write_logs(msg, level="info")
|
||||||
else:
|
else:
|
||||||
|
@ -691,8 +716,11 @@ class ResticRunner:
|
||||||
self.write_logs(msg, level="error")
|
self.write_logs(msg, level="error")
|
||||||
if output:
|
if output:
|
||||||
try:
|
try:
|
||||||
js["output"] = json.loads(output)
|
js["output"] = msgspec.json.decode(output)
|
||||||
except json.decoder.JSONDecodeError:
|
except msgspec.DecodeError:
|
||||||
|
msg = f"JSON decode error: {exc} on output '{output}'"
|
||||||
|
self.write_logs(msg, level="error")
|
||||||
|
js["extended_info"] = msg
|
||||||
js["output"] = {"data": output}
|
js["output"] = {"data": output}
|
||||||
return js
|
return js
|
||||||
|
|
||||||
|
|
37
npbackup/restic_wrapper/schema.py
Normal file
37
npbackup/restic_wrapper/schema.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# This file is part of npbackup
|
||||||
|
|
||||||
|
__intname__ = "npbackup.restic_wrapper.schema"
|
||||||
|
__author__ = "Orsiris de Jong"
|
||||||
|
__copyright__ = "Copyright (C) 2024 NetInvent"
|
||||||
|
__license__ = "GPL-3.0-only"
|
||||||
|
__description__ = "Restic json output schemas"
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from enum import StrEnum
|
||||||
|
from datetime import datetime
|
||||||
|
from msgspec import Struct
|
||||||
|
|
||||||
|
|
||||||
|
class LsNodeType(StrEnum):
|
||||||
|
FILE = "file"
|
||||||
|
DIR = "dir"
|
||||||
|
SYMLINK = "symlink"
|
||||||
|
IRREGULAR = "irregular"
|
||||||
|
|
||||||
|
|
||||||
|
class LsNode(Struct, omit_defaults=True):
|
||||||
|
"""
|
||||||
|
restic ls outputs lines of
|
||||||
|
{"name": "b458b848.2024-04-28-13h07.gz", "type": "file", "path": "/path/b458b848.2024-04-28-13h07.gz", "uid": 0, "gid": 0, "size": 82638431, "mode": 438, "permissions": "-rw-rw-rw-", "mtime": "2024-04-29T10:32:18+02:00", "atime": "2024-04-29T10:32:18+02:00", "ctime": "2024-04-29T10:32:18+02:00", "message_type": "node", "struct_type": "node"}
|
||||||
|
# In order to save some memory in GUI, let's drop unused data
|
||||||
|
"""
|
||||||
|
|
||||||
|
# name: str # We don't need name, we have path from which we extract name, which is more memory efficient
|
||||||
|
type: LsNodeType
|
||||||
|
path: str
|
||||||
|
mtime: datetime
|
||||||
|
size: Optional[int] = None
|
|
@ -50,6 +50,8 @@ OEM_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAAXNSR0IArs4c6QAAAAR
|
||||||
FOLDER_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAABnUlEQVQ4y8WSv2rUQRSFv7vZgJFFsQg2EkWb4AvEJ8hqKVilSmFn3iNvIAp21oIW9haihBRKiqwElMVsIJjNrprsOr/5dyzml3UhEQIWHhjmcpn7zblw4B9lJ8Xag9mlmQb3AJzX3tOX8Tngzg349q7t5xcfzpKGhOFHnjx+9qLTzW8wsmFTL2Gzk7Y2O/k9kCbtwUZbV+Zvo8Md3PALrjoiqsKSR9ljpAJpwOsNtlfXfRvoNU8Arr/NsVo0ry5z4dZN5hoGqEzYDChBOoKwS/vSq0XW3y5NAI/uN1cvLqzQur4MCpBGEEd1PQDfQ74HYR+LfeQOAOYAmgAmbly+dgfid5CHPIKqC74L8RDyGPIYy7+QQjFWa7ICsQ8SpB/IfcJSDVMAJUwJkYDMNOEPIBxA/gnuMyYPijXAI3lMse7FGnIKsIuqrxgRSeXOoYZUCI8pIKW/OHA7kD2YYcpAKgM5ABXk4qSsdJaDOMCsgTIYAlL5TQFTyUIZDmev0N/bnwqnylEBQS45UKnHx/lUlFvA3fo+jwR8ALb47/oNma38cuqiJ9AAAAAASUVORK5CYII="
|
FOLDER_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAABnUlEQVQ4y8WSv2rUQRSFv7vZgJFFsQg2EkWb4AvEJ8hqKVilSmFn3iNvIAp21oIW9haihBRKiqwElMVsIJjNrprsOr/5dyzml3UhEQIWHhjmcpn7zblw4B9lJ8Xag9mlmQb3AJzX3tOX8Tngzg349q7t5xcfzpKGhOFHnjx+9qLTzW8wsmFTL2Gzk7Y2O/k9kCbtwUZbV+Zvo8Md3PALrjoiqsKSR9ljpAJpwOsNtlfXfRvoNU8Arr/NsVo0ry5z4dZN5hoGqEzYDChBOoKwS/vSq0XW3y5NAI/uN1cvLqzQur4MCpBGEEd1PQDfQ74HYR+LfeQOAOYAmgAmbly+dgfid5CHPIKqC74L8RDyGPIYy7+QQjFWa7ICsQ8SpB/IfcJSDVMAJUwJkYDMNOEPIBxA/gnuMyYPijXAI3lMse7FGnIKsIuqrxgRSeXOoYZUCI8pIKW/OHA7kD2YYcpAKgM5ABXk4qSsdJaDOMCsgTIYAlL5TQFTyUIZDmev0N/bnwqnylEBQS45UKnHx/lUlFvA3fo+jwR8ALb47/oNma38cuqiJ9AAAAAASUVORK5CYII="
|
||||||
INHERITED_FOLDER_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAJCSURBVDhPxVJNaBNhEH3bJPSHtCWtpRZbU4shJQoigi2IOfTQ2F5EUUEP2oNa0N5E8eBJUEHUg0IxGPAg6EGKeigo9CBSbEuVLNFkIWot1KYxzV/TNt1sdvdzvk3MehARPPhgdub7dt7bN8PiXyGUM0aO2HosVRjgtaywRf9z9REv+flPqAgk3vqUlu7TNmgZFDMi7o8GxqR5/TV16AJvq3QCs5IWmpX0KSq1ynV6xsccLR6wVBRyZg5yYRUqK0DQFDBdIb5WEqkCxmcQGbqh+Oj0rSIQG+9g9c17YW31orqxC5PTYbyceI9Pc0twuzpw6ngftjvrgeIC4pEXelvf5DGijZEecP6Qdahu6wnYPedQ0+TG9duP4R28DHdXAy6c2Y12x3fs6B0GS9wDlgNgcoTTavnDEBAYOhvbewE1CzEYxJVrTzA/PYKTgwX0uD5j+LAGWdxD46yAsSLAOKsEQ8CAmqRIIfwxhH7vFjg3LdGKVhCOZihyECOrxKM9QKcwFazlTLMt07scErEI2ppJV5fx6k0MNx98RTqnwLnZimd3W4mrUjMXKcF0QMth+Q/o3maBKGWpcQP9++yYeOjG2aMO6EynUbl97sJ0YArIUbKcw8B+O6ptDLcCX4iwYThZW1eoQaf5KRsiv3OgpomQpw+s4+mdTkwFsxA877DzoISro0nscllIg5MpfhmhsoPkYrxcCagji/5LtfBfrKH5i2hqoGuynoqTIyLn10yBnz+SneJAOf8N+BKCFCHj9B8B/ACq5vDyyzK0kwAAAABJRU5ErkJggg=="
|
INHERITED_FOLDER_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAJCSURBVDhPxVJNaBNhEH3bJPSHtCWtpRZbU4shJQoigi2IOfTQ2F5EUUEP2oNa0N5E8eBJUEHUg0IxGPAg6EGKeigo9CBSbEuVLNFkIWot1KYxzV/TNt1sdvdzvk3MehARPPhgdub7dt7bN8PiXyGUM0aO2HosVRjgtaywRf9z9REv+flPqAgk3vqUlu7TNmgZFDMi7o8GxqR5/TV16AJvq3QCs5IWmpX0KSq1ynV6xsccLR6wVBRyZg5yYRUqK0DQFDBdIb5WEqkCxmcQGbqh+Oj0rSIQG+9g9c17YW31orqxC5PTYbyceI9Pc0twuzpw6ngftjvrgeIC4pEXelvf5DGijZEecP6Qdahu6wnYPedQ0+TG9duP4R28DHdXAy6c2Y12x3fs6B0GS9wDlgNgcoTTavnDEBAYOhvbewE1CzEYxJVrTzA/PYKTgwX0uD5j+LAGWdxD46yAsSLAOKsEQ8CAmqRIIfwxhH7vFjg3LdGKVhCOZihyECOrxKM9QKcwFazlTLMt07scErEI2ppJV5fx6k0MNx98RTqnwLnZimd3W4mrUjMXKcF0QMth+Q/o3maBKGWpcQP9++yYeOjG2aMO6EynUbl97sJ0YArIUbKcw8B+O6ptDLcCX4iwYThZW1eoQaf5KRsiv3OgpomQpw+s4+mdTkwFsxA877DzoISro0nscllIg5MpfhmhsoPkYrxcCagji/5LtfBfrKH5i2hqoGuynoqTIyLn10yBnz+SneJAOf8N+BKCFCHj9B8B/ACq5vDyyzK0kwAAAABJRU5ErkJggg=="
|
||||||
FILE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAABU0lEQVQ4y52TzStEURiHn/ecc6XG54JSdlMkNhYWsiILS0lsJaUsLW2Mv8CfIDtr2VtbY4GUEvmIZnKbZsY977Uwt2HcyW1+dTZvt6fn9557BGB+aaNQKBR2ifkbgWR+cX13ubO1svz++niVTA1ArDHDg91UahHFsMxbKWycYsjze4muTsP64vT43v7hSf/A0FgdjQPQWAmco68nB+T+SFSqNUQgcIbN1bn8Z3RwvL22MAvcu8TACFgrpMVZ4aUYcn77BMDkxGgemAGOHIBXxRjBWZMKoCPA2h6qEUSRR2MF6GxUUMUaIUgBCNTnAcm3H2G5YQfgvccYIXAtDH7FoKq/AaqKlbrBj2trFVXfBPAea4SOIIsBeN9kkCwxsNkAqRWy7+B7Z00G3xVc2wZeMSI4S7sVYkSk5Z/4PyBWROqvox3A28PN2cjUwinQC9QyckKALxj4kv2auK0xAAAAAElFTkSuQmCC"
|
FILE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAABU0lEQVQ4y52TzStEURiHn/ecc6XG54JSdlMkNhYWsiILS0lsJaUsLW2Mv8CfIDtr2VtbY4GUEvmIZnKbZsY977Uwt2HcyW1+dTZvt6fn9557BGB+aaNQKBR2ifkbgWR+cX13ubO1svz++niVTA1ArDHDg91UahHFsMxbKWycYsjze4muTsP64vT43v7hSf/A0FgdjQPQWAmco68nB+T+SFSqNUQgcIbN1bn8Z3RwvL22MAvcu8TACFgrpMVZ4aUYcn77BMDkxGgemAGOHIBXxRjBWZMKoCPA2h6qEUSRR2MF6GxUUMUaIUgBCNTnAcm3H2G5YQfgvccYIXAtDH7FoKq/AaqKlbrBj2trFVXfBPAea4SOIIsBeN9kkCwxsNkAqRWy7+B7Z00G3xVc2wZeMSI4S7sVYkSk5Z/4PyBWROqvox3A28PN2cjUwinQC9QyckKALxj4kv2auK0xAAAAAElFTkSuQmCC"
|
||||||
|
SYMLINK_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAwUExURf///0lJSSgoKNDQ0AAAAMfHxxoaGsTExNfX1/b29hcXFy8vL+zs7MHBwSsrK+jo6JAyYacAAAAJcEhZcwAACxEAAAsRAX9kX5EAAABXSURBVBjTY8AChJSUlAxADBUXF5cAMMOtomMCmLFkDwNnMIhxz/sAhxOI0f9kD4TR8M/7BITB/2Q7iGE6geGfG4jBwNnRuwTMYAWaCGYwA+1QxLCZgQEAN90WKol5c8kAAAAASUVORK5CYII="
|
||||||
|
IRREGULAR_FILE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAwUExURQAAABISEjg4OEBAQF5eXmdnZ2lpaXl5eYODg46OjpWVlaWlpbGxsdPT0/X19f///y10hqsAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAA1SURBVBjTY/gPBdgZv/KgjI/cUMbPOAjjR3kthPGZgQcq9YUw49citrVgxgcGBi5MS///BwC5AXGMCId5ZAAAAABJRU5ErkJggg=="
|
||||||
INHERITED_FILE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAJFSURBVDhPjVNNaBNREP6y2cRo29QfGmJNayBBWxWr+I/gP9VYxFLUnITVFgqCF8GLFyuKeND6g/UkSA8qBC9KDxYVUQQx1otNQ2q1VAWtUcMWQlvT7K4zb7NpdAv6wbe8mfdm5pt5bx0g7Gpp6yCchsHWX+ATBX/i3cjAqWOHDmS+fxm0vBJ/DN1AwFeByVweanYcP8ey01Sz+JYZQ7lHwtHmTcvPXO7umVe1sJ7CRHGZP7qhwyXLmOstI4v5JyZ/5eCg4y5ZQnt0R2gqf+vBCWXPdtr6XFQg0QGn0zEjZWKalPR/GEXqYxqrVywNUdhmjhUKNF2HRBlkp8gn8Cb+Ei+ePcankWHUBkPYu/8gKfQin9eEYoKHPyJCpwROSuCiBMybXZ1Qok0Ih5egrf04AoEaRPdtRcUcDyrLZ0PTRAIBU4GmCQXcYzLxFlcvnsPzeAKLArXi0Jp163FYaRVrrskFLUwroClx9eH3KWzZthPBxUHTHkoJDib7iwp1XRPBDPMWSAG34HZJUDM/4Pf7xfrpk0foutYJVVVJTQ26b8dEUGkLQoE1RM5eV1eP5IBZrbFxN+739EI50kqXbpQomKEFawaRSASz3G7cuH5F2MyJiXG6ZnOfyTOzUFBgtsDXyLxzN4a++Ct4y9zYsHYVLpw/i5UNDcX9UgUCyzY2dVBfRm5Ks/HraNrmiz18zf+BwrGFFgx6qvzqJBt9viqbzz4Deln81qmL/2JpAjKBBdWhyPzq8ElaVhJz7PsXhvp6LwG49xvFxfylO2UHaAAAAABJRU5ErkJggg=="
|
INHERITED_FILE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAJFSURBVDhPjVNNaBNREP6y2cRo29QfGmJNayBBWxWr+I/gP9VYxFLUnITVFgqCF8GLFyuKeND6g/UkSA8qBC9KDxYVUQQx1otNQ2q1VAWtUcMWQlvT7K4zb7NpdAv6wbe8mfdm5pt5bx0g7Gpp6yCchsHWX+ATBX/i3cjAqWOHDmS+fxm0vBJ/DN1AwFeByVweanYcP8ey01Sz+JYZQ7lHwtHmTcvPXO7umVe1sJ7CRHGZP7qhwyXLmOstI4v5JyZ/5eCg4y5ZQnt0R2gqf+vBCWXPdtr6XFQg0QGn0zEjZWKalPR/GEXqYxqrVywNUdhmjhUKNF2HRBlkp8gn8Cb+Ei+ePcankWHUBkPYu/8gKfQin9eEYoKHPyJCpwROSuCiBMybXZ1Qok0Ih5egrf04AoEaRPdtRcUcDyrLZ0PTRAIBU4GmCQXcYzLxFlcvnsPzeAKLArXi0Jp163FYaRVrrskFLUwroClx9eH3KWzZthPBxUHTHkoJDib7iwp1XRPBDPMWSAG34HZJUDM/4Pf7xfrpk0foutYJVVVJTQ26b8dEUGkLQoE1RM5eV1eP5IBZrbFxN+739EI50kqXbpQomKEFawaRSASz3G7cuH5F2MyJiXG6ZnOfyTOzUFBgtsDXyLxzN4a++Ct4y9zYsHYVLpw/i5UNDcX9UgUCyzY2dVBfRm5Ks/HraNrmiz18zf+BwrGFFgx6qvzqJBt9viqbzz4Deln81qmL/2JpAjKBBdWhyPzq8ElaVhJz7PsXhvp6LwG49xvFxfylO2UHaAAAAABJRU5ErkJggg=="
|
||||||
TREE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAEtSURBVDhPjZNdTsJAFIUPbalQUSEQNMQ16AJcga++uBJe9AE2wE5civvwzUDQhIClnjMz5Wc6gd7km85M7px7ettpgPH4Ukxfp5ig0MoLZZT7JtvG20PDrCINxRboD4HNGvj9AZaLA+bA/Hs/L2HsBbaskCTA5TXQG1Ds9phuHxjeAVddi4t7DUYAdCBDURwmJgtWzTKLiycN1oEEOAsdFs3Uutv8AasV0GrrFFoadgIRZwmTQ6QXQJuVRdZhfq5TNqwANyQQsw/nkBs1vQwjoI2IPTAVmeQ78KkKOAdJk0hAzxPk/ivkronqdh3CryAHql6DahOdQOgThgj2QD9SyG4IFSxj5+DUn+hTdVDwMlBAInU4FOCSF2T0/twZTcac3hDeyfPx9ZnOAHz8A2r8W8iBwGS0AAAAAElFTkSuQmCC"
|
TREE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAEtSURBVDhPjZNdTsJAFIUPbalQUSEQNMQ16AJcga++uBJe9AE2wE5civvwzUDQhIClnjMz5Wc6gd7km85M7px7ettpgPH4Ukxfp5ig0MoLZZT7JtvG20PDrCINxRboD4HNGvj9AZaLA+bA/Hs/L2HsBbaskCTA5TXQG1Ds9phuHxjeAVddi4t7DUYAdCBDURwmJgtWzTKLiycN1oEEOAsdFs3Uutv8AasV0GrrFFoadgIRZwmTQ6QXQJuVRdZhfq5TNqwANyQQsw/nkBs1vQwjoI2IPTAVmeQ78KkKOAdJk0hAzxPk/ivkronqdh3CryAHql6DahOdQOgThgj2QD9SyG4IFSxj5+DUn+hTdVDwMlBAInU4FOCSF2T0/twZTcac3hDeyfPx9ZnOAHz8A2r8W8iBwGS0AAAAAElFTkSuQmCC"
|
||||||
INHERITED_TREE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAIKSURBVDhPjVPPS1RRFP7mzTj425GXM5MNjpHgQNKEP3AhWEE0zCKCNoKbapO7IETBH6BBtYiZTeqfIC5UcCGooItoEQxBRU0tNBgYUBLHRnQQtDfPc857b3w1E/XBdznnvnO/+91z33WAEL6vT/ZNYgI6Z3+AK6x5qTYwes0hmcKDngdUL3B6AuSOgMMDG7NAdv88tkg4F8jTDi4XUFUL1F8gMd/v9KiA1w/UeAyaCPAgAiAHbEhxlqaTeEC7VlYaNNHDA+1L61mApLjYwvfEW3x7s4q91CYamlvRfu8BOWyB9gsor5CSch6MI5CAQpGLBJjrMy8w1deLiy2tuD0wCDUQQPxuGyrYQTXVa7zKgOGAJljASVn6y0csx8bxPJGihUEputLVjRsPByRmk9x0C+KAJxTqAe/+YyuJqzcj8AWDRr6ZFG5//VBwaBcQB7rpwFVG17i/C4/fL/HnjTWsvH6FXPYnuWnCk9klWaTZjiAONLOJ3O1LoRDSyU8SX78TwcjyBm49ekxVuswxSx+BHZCfjmgUZW43VqdjkjNPj4/kmq3c3kQRsG7Buvenc/PYSrxDf5UDw51tWHz5DM3hcOF7yR7wDmyP4bvchKGFRYkPMxnUqPQr2sAbWig4+NufWOdVi+aKe6DTYyABFvkf2gUopQfSOBatbpwYorCOSG/y39h5744DWDgDprSgrrE6IGUAAAAASUVORK5CYII="
|
INHERITED_TREE_ICON = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACxEAAAsRAX9kX5EAAAIKSURBVDhPjVPPS1RRFP7mzTj425GXM5MNjpHgQNKEP3AhWEE0zCKCNoKbapO7IETBH6BBtYiZTeqfIC5UcCGooItoEQxBRU0tNBgYUBLHRnQQtDfPc857b3w1E/XBdznnvnO/+91z33WAEL6vT/ZNYgI6Z3+AK6x5qTYwes0hmcKDngdUL3B6AuSOgMMDG7NAdv88tkg4F8jTDi4XUFUL1F8gMd/v9KiA1w/UeAyaCPAgAiAHbEhxlqaTeEC7VlYaNNHDA+1L61mApLjYwvfEW3x7s4q91CYamlvRfu8BOWyB9gsor5CSch6MI5CAQpGLBJjrMy8w1deLiy2tuD0wCDUQQPxuGyrYQTXVa7zKgOGAJljASVn6y0csx8bxPJGihUEputLVjRsPByRmk9x0C+KAJxTqAe/+YyuJqzcj8AWDRr6ZFG5//VBwaBcQB7rpwFVG17i/C4/fL/HnjTWsvH6FXPYnuWnCk9klWaTZjiAONLOJ3O1LoRDSyU8SX78TwcjyBm49ekxVuswxSx+BHZCfjmgUZW43VqdjkjNPj4/kmq3c3kQRsG7Buvenc/PYSrxDf5UDw51tWHz5DM3hcOF7yR7wDmyP4bvchKGFRYkPMxnUqPQr2sAbWig4+NufWOdVi+aKe6DTYyABFvkf2gUopQfSOBatbpwYorCOSG/y39h5744DWDgDprSgrrE6IGUAAAAASUVORK5CYII="
|
||||||
|
|
BIN
resources/irregular_file_icon.png
Normal file
BIN
resources/irregular_file_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 220 B |
BIN
resources/symlink_icon.png
Normal file
BIN
resources/symlink_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 254 B |
Loading…
Reference in a new issue