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"
|
||||
__copyright__ = "Copyright (C) 2022-2024 NetInvent"
|
||||
__license__ = "GPL-3.0-only"
|
||||
__build__ = "2024061101"
|
||||
__build__ = "2024091501"
|
||||
|
||||
|
||||
from typing import Optional, Callable, Union, List
|
||||
|
@ -220,6 +220,9 @@ class NPBackupRunner:
|
|||
self._verbose = False
|
||||
self._live_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._no_cache = False
|
||||
self.restic_runner = None
|
||||
|
@ -300,6 +303,17 @@ class NPBackupRunner:
|
|||
self.write_logs(msg, level="critical", raise_error="ValueError")
|
||||
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
|
||||
def stdout(self):
|
||||
return self._stdout
|
||||
|
@ -409,7 +423,7 @@ class NPBackupRunner:
|
|||
f"Runner took {self.exec_time} seconds for {fn.__name__}", level="info"
|
||||
)
|
||||
try:
|
||||
os.environ['NPBACKUP_EXEC_TIME'] = str(self.exec_time)
|
||||
os.environ["NPBACKUP_EXEC_TIME"] = str(self.exec_time)
|
||||
except OSError:
|
||||
pass
|
||||
return result
|
||||
|
@ -826,6 +840,7 @@ class NPBackupRunner:
|
|||
self.restic_runner.no_cache = self.no_cache
|
||||
self.restic_runner.live_output = self.live_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.stderr = self.stderr
|
||||
if self.binary:
|
||||
|
|
|
@ -13,6 +13,7 @@ from typing import List, Optional, Tuple
|
|||
import sys
|
||||
import os
|
||||
import re
|
||||
import gc
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
from logging import getLogger
|
||||
|
@ -41,6 +42,8 @@ from resources.customization import (
|
|||
LOADER_ANIMATION,
|
||||
FOLDER_ICON,
|
||||
FILE_ICON,
|
||||
SYMLINK_ICON,
|
||||
IRREGULAR_FILE_ICON,
|
||||
LICENSE_TEXT,
|
||||
SIMPLEGUI_THEME,
|
||||
OEM_ICON,
|
||||
|
@ -55,6 +58,7 @@ from npbackup.path_helper import CURRENT_DIR
|
|||
from npbackup.__version__ import version_string
|
||||
from npbackup.__debug__ import _DEBUG
|
||||
from npbackup.restic_wrapper import ResticRunner
|
||||
from npbackup.restic_wrapper import schema
|
||||
|
||||
|
||||
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': '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()
|
||||
count = 0
|
||||
for entry in ls_result:
|
||||
# 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":
|
||||
# On windows, we need to make sure tree keys don't get duplicate because of lower/uppercase
|
||||
# Shown filenames aren't affected by this
|
||||
entry["path"] = entry["path"].lower()
|
||||
parent = os.path.dirname(entry["path"])
|
||||
entry.path = entry.path.lower()
|
||||
parent = os.path.dirname(entry.path)
|
||||
|
||||
# Make sure we normalize mtime, and remove microseconds
|
||||
# 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 = str(entry["mtime"])[0:19]
|
||||
if entry["type"] == "dir" and entry["path"] not in treedata.tree_dict:
|
||||
mtime = entry.mtime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
name = os.path.basename(entry.path)
|
||||
if entry.type == schema.LsNodeType.DIR and entry.path not in treedata.tree_dict:
|
||||
treedata.Insert(
|
||||
parent=parent,
|
||||
key=entry["path"],
|
||||
text=entry["name"],
|
||||
key=entry.path,
|
||||
text=name,
|
||||
values=["", mtime],
|
||||
icon=FOLDER_ICON,
|
||||
)
|
||||
elif entry["type"] == "file":
|
||||
size = BytesConverter(entry["size"]).human
|
||||
elif entry.type == schema.LsNodeType.FILE:
|
||||
size = BytesConverter(entry.size).human
|
||||
treedata.Insert(
|
||||
parent=parent,
|
||||
key=entry["path"],
|
||||
text=entry["name"],
|
||||
key=entry.path,
|
||||
text=name,
|
||||
values=[size, mtime],
|
||||
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
|
||||
# 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
|
||||
|
@ -237,12 +262,13 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
|||
__backend_binary=backend_binary,
|
||||
)
|
||||
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
|
||||
|
||||
# result is {"result": True, "output": [{snapshot_description}, {entry}, {entry}]}
|
||||
content = result["output"]
|
||||
# content = result["output"]
|
||||
# First entry of snapshot list is the snapshot description
|
||||
snapshot = content.pop(0)
|
||||
snapshot = result["output"].pop(0)
|
||||
try:
|
||||
snap_date = dateutil.parser.parse(snapshot["time"])
|
||||
except (KeyError, IndexError, TypeError):
|
||||
|
@ -261,7 +287,6 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
|||
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}"
|
||||
|
||||
if not backup_id or not snapshot or not short_id:
|
||||
sg.PopupError(_t("main_gui.cannot_get_content"), keep_on_top=True)
|
||||
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
|
||||
# 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():
|
||||
sg.PopupAnimated(
|
||||
LOADER_ANIMATION,
|
||||
|
@ -283,6 +309,8 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
|||
)
|
||||
sg.PopupAnimated(None)
|
||||
|
||||
logger.info("Finished creating data tree")
|
||||
|
||||
left_col = [
|
||||
[sg.Text(backup_id)],
|
||||
[
|
||||
|
@ -316,8 +344,11 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
|||
enable_close_attempted_event=True,
|
||||
)
|
||||
|
||||
# Reclaim memory fro thread result
|
||||
thread = None
|
||||
# Reclaim memory from thread result
|
||||
# Note from v3 dev: This doesn't actually improve memory usage
|
||||
del thread
|
||||
del result
|
||||
gc.collect()
|
||||
|
||||
while True:
|
||||
event, values = window.read()
|
||||
|
@ -334,6 +365,7 @@ def ls_window(repo_config: dict, snapshot_id: str) -> bool:
|
|||
# before closing the window
|
||||
window["-TREE-"].update(values=sg.TreeData())
|
||||
window.close()
|
||||
del window
|
||||
return True
|
||||
|
||||
|
||||
|
@ -966,6 +998,7 @@ def _main_gui(viewer_mode: bool):
|
|||
continue
|
||||
snapshot_to_see = snapshot_list[values["snapshot-list"][0]][0]
|
||||
ls_window(repo_config, snapshot_to_see)
|
||||
gc.collect()
|
||||
if event == "--FORGET--":
|
||||
if not full_config:
|
||||
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
|
||||
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
|
||||
if __repo_config:
|
||||
runner.repo_config = __repo_config
|
||||
|
|
|
@ -7,8 +7,8 @@ __intname__ = "npbackup.restic_wrapper"
|
|||
__author__ = "Orsiris de Jong"
|
||||
__copyright__ = "Copyright (C) 2022-2024 NetInvent"
|
||||
__license__ = "GPL-3.0-only"
|
||||
__build__ = "2024090602"
|
||||
__version__ = "2.2.4"
|
||||
__build__ = "2024091501"
|
||||
__version__ = "2.3.0"
|
||||
|
||||
|
||||
from typing import Tuple, List, Optional, Callable, Union
|
||||
|
@ -16,7 +16,7 @@ import os
|
|||
import sys
|
||||
from logging import getLogger
|
||||
import re
|
||||
import json
|
||||
import msgspec
|
||||
from datetime import datetime, timezone
|
||||
import dateutil.parser
|
||||
import queue
|
||||
|
@ -26,7 +26,7 @@ from ofunctions.misc import BytesConverter, fn_name
|
|||
from npbackup.__debug__ import _DEBUG
|
||||
from npbackup.__env__ import FAST_COMMANDS_TIMEOUT, CHECK_INTERVAL
|
||||
from npbackup.path_helper import CURRENT_DIR
|
||||
|
||||
from npbackup.restic_wrapper import schema
|
||||
|
||||
logger = getLogger()
|
||||
|
||||
|
@ -48,6 +48,7 @@ class ResticRunner:
|
|||
self._dry_run = False
|
||||
self._no_cache = False
|
||||
self._json_output = False
|
||||
self._struct_output = False
|
||||
|
||||
self.backup_result_content = None
|
||||
|
||||
|
@ -222,6 +223,17 @@ class ResticRunner:
|
|||
else:
|
||||
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
|
||||
def ignore_cloud_files(self) -> bool:
|
||||
return self._ignore_cloud_files
|
||||
|
@ -636,6 +648,10 @@ class ResticRunner:
|
|||
return self.convert_to_json_output(False, output=msg)
|
||||
else:
|
||||
# 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)
|
||||
return self.convert_to_json_output(False, msg=msg)
|
||||
# pylint: disable=E1102 (not-callable)
|
||||
|
@ -658,31 +674,40 @@ class ResticRunner:
|
|||
js = {
|
||||
"result": result,
|
||||
"operation": operation,
|
||||
"extended_info": None,
|
||||
"args": kwargs,
|
||||
"output": None,
|
||||
"output": [],
|
||||
}
|
||||
if result:
|
||||
if output:
|
||||
if isinstance(output, str):
|
||||
output = list(filter(None, output.split("\n")))
|
||||
else:
|
||||
output = [str(output)]
|
||||
if len(output) > 1:
|
||||
output_is_list = True
|
||||
js["output"] = []
|
||||
else:
|
||||
output_is_list = False
|
||||
for line in output:
|
||||
if output_is_list:
|
||||
decoder = msgspec.json.Decoder()
|
||||
ls_decoder = msgspec.json.Decoder(schema.LsNode)
|
||||
is_first_line = True
|
||||
|
||||
for line in output.split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
js["output"].append(json.loads(line))
|
||||
except json.decoder.JSONDecodeError:
|
||||
js["output"].append(line)
|
||||
if (
|
||||
not is_first_line
|
||||
and operation == "ls"
|
||||
and self.struct_output
|
||||
):
|
||||
|
||||
js["output"].append(ls_decoder.decode(line))
|
||||
else:
|
||||
try:
|
||||
js["output"] = json.loads(line)
|
||||
except json.decoder.JSONDecodeError:
|
||||
js["output"] = {"data": line}
|
||||
js["output"].append(decoder.decode(line))
|
||||
is_first_line = False
|
||||
except msgspec.DecodeError as exc:
|
||||
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:
|
||||
self.write_logs(msg, level="info")
|
||||
else:
|
||||
|
@ -691,8 +716,11 @@ class ResticRunner:
|
|||
self.write_logs(msg, level="error")
|
||||
if output:
|
||||
try:
|
||||
js["output"] = json.loads(output)
|
||||
except json.decoder.JSONDecodeError:
|
||||
js["output"] = msgspec.json.decode(output)
|
||||
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}
|
||||
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="
|
||||
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"
|
||||
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=="
|
||||
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="
|
||||
|
|
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