From 7aa133be2a90ff939b2c202a55ca9c7e96fb2071 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Vin=C3=ADcius=20Moreira?=
Date: Wed, 27 May 2020 16:18:53 -0300
Subject: [PATCH] [feature] axel support for multithreaded downloads
---
CHANGELOG.md | 7 ++
README.md | 3 +-
bauh/api/abstract/download.py | 10 +--
bauh/gems/appimage/controller.py | 10 +--
bauh/manage.py | 2 +-
bauh/view/core/config.py | 1 +
bauh/view/core/controller.py | 3 +-
bauh/view/core/downloader.py | 87 ++++++++++++++++++-----
bauh/view/core/settings.py | 29 +++++++-
bauh/view/resources/locale/ca | 4 +-
bauh/view/resources/locale/de | 4 +-
bauh/view/resources/locale/en | 4 +-
bauh/view/resources/locale/es | 4 +-
bauh/view/resources/locale/it | 4 +-
bauh/view/resources/locale/pt | 4 +-
bauh/view/resources/locale/ru | 4 +-
bauh/view/resources/locale/tr | 4 +-
pictures/releases/0.9.4/mthread_tool.png | Bin 0 -> 16585 bytes
18 files changed, 139 insertions(+), 45 deletions(-)
create mode 100644 pictures/releases/0.9.4/mthread_tool.png
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27aa801b..24e53d80 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+- Settings
+ - [axel](https://github.com/axel-download-accelerator/axel) added as an alternative multi-threaded download tool. The tool can be defined through the new field **Multi-threaded download tool** on the settings window **Advanced** tab
+
+
+
+
+
### Improvements
- Arch
diff --git a/README.md b/README.md
index 5b2f37dc..74a70996 100644
--- a/README.md
+++ b/README.md
@@ -257,7 +257,8 @@ You can change some application settings via environment variables or arguments
```
download:
icons: true # allows bauh to download the applications icons when they are not saved on the disk
- multithreaded: true # allows bauh to use a multithreaded download client installed on the system to download applications source files faster ( current only **aria2** is supported )
+ multithreaded: true # allows bauh to use a multithreaded download client installed on the system to download applications source files faster
+ multithreaded_client: null # defines the multi-threaded download tool to be used. If null, the default installed tool will be used (priority: aria2 > axel). Possible tools/values: aria2, axel
gems: null # defines the enabled applications types managed by bauh ( a null value means all available )
locale: null # defines a different translation for bauh ( a null value will retrieve the system's default locale )
store_root_password: true # if the root password should be asked only once
diff --git a/bauh/api/abstract/download.py b/bauh/api/abstract/download.py
index 5a665072..a90a8dd1 100644
--- a/bauh/api/abstract/download.py
+++ b/bauh/api/abstract/download.py
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
+from typing import Iterable
from bauh.api.abstract.handler import ProcessWatcher
@@ -26,8 +27,9 @@ class FileDownloader(ABC):
pass
@abstractmethod
- def get_default_client_name(self) -> str:
- """
- :return: retrieve current downloader client name
- """
+ def can_work(self) -> bool:
+ pass
+
+ @abstractmethod
+ def get_supported_multithreaded_clients(self) -> Iterable[str]:
pass
diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py
index b39b3e1f..12b8c522 100644
--- a/bauh/gems/appimage/controller.py
+++ b/bauh/gems/appimage/controller.py
@@ -517,16 +517,8 @@ class AppImageManager(SoftwareManager):
res = run_cmd('which sqlite3')
return res and not res.strip().startswith('which ')
- def _is_wget_available(self):
- res = run_cmd('which wget')
- return res and not res.strip().startswith('which ')
-
- def _is_aria2_available(self):
- res = run_cmd('which aria2c')
- return res and not res.strip().startswith('which ')
-
def can_work(self) -> bool:
- return self._is_sqlite3_available() and (self._is_wget_available() or self._is_aria2_available())
+ return self._is_sqlite3_available() and self.file_downloader.can_work()
def requires_root(self, action: str, pkg: AppImage):
return False
diff --git a/bauh/manage.py b/bauh/manage.py
index 28c71fe5..60fe696c 100644
--- a/bauh/manage.py
+++ b/bauh/manage.py
@@ -38,7 +38,7 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg
logger=logger,
distro=util.get_distro(),
file_downloader=AdaptableFileDownloader(logger, bool(app_config['download']['multithreaded']),
- i18n, http_client),
+ i18n, http_client, app_config['download']['multithreaded_client']),
app_name=__app_name__)
managers = gems.load_managers(context=context, locale=i18n.current_key, config=app_config, default_locale=DEFAULT_I18N_KEY)
diff --git a/bauh/view/core/config.py b/bauh/view/core/config.py
index 938a5d81..2f843b9a 100644
--- a/bauh/view/core/config.py
+++ b/bauh/view/core/config.py
@@ -44,6 +44,7 @@ def read_config(update_file: bool = False) -> dict:
},
'download': {
'multithreaded': True,
+ 'multithreaded_client': None,
'icons': True
},
'store_root_password': True,
diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py
index 431dfe71..be908ca8 100755
--- a/bauh/view/core/controller.py
+++ b/bauh/view/core/controller.py
@@ -432,7 +432,8 @@ class GenericSoftwareManager(SoftwareManager):
self.settings_manager = GenericSettingsManager(managers=self.managers,
working_managers=self.working_managers,
logger=self.logger,
- i18n=self.i18n)
+ i18n=self.i18n,
+ file_downloader=self.context.file_downloader)
else:
self.settings_manager.managers = self.managers
self.settings_manager.working_managers = self.working_managers
diff --git a/bauh/view/core/downloader.py b/bauh/view/core/downloader.py
index 1dfbf7b0..7aecfa0e 100644
--- a/bauh/view/core/downloader.py
+++ b/bauh/view/core/downloader.py
@@ -5,6 +5,7 @@ import time
import traceback
from math import floor
from threading import Thread
+from typing import Iterable
from bauh.api.abstract.download import FileDownloader
from bauh.api.abstract.handler import ProcessWatcher
@@ -18,27 +19,27 @@ RE_HAS_EXTENSION = re.compile(r'.+\.\w+$')
class AdaptableFileDownloader(FileDownloader):
- def __init__(self, logger: logging.Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient):
+ def __init__(self, logger: logging.Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient, multithread_client: str):
self.logger = logger
self.multithread_enabled = multithread_enabled
self.i18n = i18n
self.http_client = http_client
+ self.supported_multithread_clients = {'aria2', 'axel'}
+ self.multithread_client = multithread_client
- def is_aria2c_available(self) -> bool:
+ @staticmethod
+ def is_aria2c_available() -> bool:
return bool(run_cmd('which aria2c', print_error=False))
- def _get_aria2c_process(self, url: str, output_path: str, cwd: str, root_password: str, max_threads: int, known_size: int) -> SimpleProcess:
+ @staticmethod
+ def is_axel_available() -> bool:
+ return bool(run_cmd('which axel', print_error=False))
- if max_threads and max_threads > 0:
- threads = max_threads
- elif known_size:
- threads = 16 if known_size >= 16000000 else floor(known_size / 1000000)
-
- if threads <= 0:
- threads = 1
- else:
- threads = 16
+ @staticmethod
+ def is_wget_available() -> bool:
+ return bool(run_cmd('which wget', print_error=False))
+ def _get_aria2c_process(self, url: str, output_path: str, cwd: str, root_password: str, threads: int) -> SimpleProcess:
cmd = ['aria2c', url,
'--no-conf',
'--max-connection-per-server={}'.format(threads),
@@ -64,6 +65,18 @@ class AdaptableFileDownloader(FileDownloader):
return SimpleProcess(cmd=cmd, root_password=root_password, cwd=cwd)
+ def _get_axel_process(self, url: str, output_path: str, cwd: str, root_password: str, threads: int) -> SimpleProcess:
+ cmd = ['axel', url,
+ '--num-connections={}'.format(threads),
+ '--ipv4',
+ '--no-clobber',
+ '--timeout=5']
+
+ if output_path:
+ cmd.append('--output={}'.format(output_path))
+
+ return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password)
+
def _get_wget_process(self, url: str, output_path: str, cwd: str, root_password: str) -> SimpleProcess:
cmd = ['wget', url, '--continue', '--retry-connrefused', '--tries=10', '--no-config']
@@ -90,6 +103,19 @@ class AdaptableFileDownloader(FileDownloader):
except:
pass
+ def _get_appropriate_threads_number(self, max_threads: int, known_size: int) -> int:
+ if max_threads and max_threads > 0:
+ threads = max_threads
+ elif known_size:
+ threads = 16 if known_size >= 16000000 else floor(known_size / 1000000)
+
+ if threads <= 0:
+ threads = 1
+ else:
+ threads = 16
+
+ return threads
+
def download(self, file_url: str, watcher: ProcessWatcher, output_path: str = None, cwd: str = None, root_password: str = None, substatus_prefix: str = None, display_file_size: bool = True, max_threads: int = None, known_size: int = None) -> bool:
self.logger.info('Downloading {}'.format(file_url))
handler = ProcessHandler(watcher)
@@ -105,10 +131,18 @@ class AdaptableFileDownloader(FileDownloader):
os.remove(output_path)
self.logger.info("Old file {} removed".format(output_path))
- if self.is_multithreaded():
- ti = time.time()
- process = self._get_aria2c_process(file_url, output_path, final_cwd, root_password, max_threads, known_size)
- downloader = 'aria2'
+ client = self.get_available_multithreaded_tool()
+ if client:
+ threads = self._get_appropriate_threads_number(max_threads, known_size)
+
+ if client == 'aria2':
+ ti = time.time()
+ process = self._get_aria2c_process(file_url, output_path, final_cwd, root_password, threads)
+ downloader = 'aria2'
+ else:
+ ti = time.time()
+ process = self._get_axel_process(file_url, output_path, final_cwd, root_password, threads)
+ downloader = 'axel'
else:
ti = time.time()
process = self._get_wget_process(file_url, output_path, final_cwd, root_password)
@@ -150,7 +184,22 @@ class AdaptableFileDownloader(FileDownloader):
return success
def is_multithreaded(self) -> bool:
- return self.multithread_enabled and self.is_aria2c_available()
+ return self.multithread_enabled and (self.is_aria2c_available() or self.is_axel_available())
- def get_default_client_name(self) -> str:
- return 'aria2c' if self. is_multithreaded() else 'wget'
+ def get_available_multithreaded_tool(self) -> str:
+ if self.multithread_enabled:
+ if self.multithread_client is None:
+ if self.is_aria2c_available():
+ return 'aria2'
+ elif self.is_axel_available():
+ return 'axel'
+ elif (self.multithread_client == 'aria2' or self.multithread_client not in self.get_supported_multithreaded_clients()) and self.is_aria2c_available():
+ return 'aria2'
+ elif self.is_axel_available():
+ return 'axel'
+
+ def can_work(self) -> bool:
+ return self.is_wget_available() or self.is_multithreaded()
+
+ def get_supported_multithreaded_clients(self) -> Iterable[str]:
+ return self.supported_multithread_clients
diff --git a/bauh/view/core/settings.py b/bauh/view/core/settings.py
index 28ffd5ea..04e3d8d3 100644
--- a/bauh/view/core/settings.py
+++ b/bauh/view/core/settings.py
@@ -8,11 +8,13 @@ from PyQt5.QtWidgets import QApplication, QStyleFactory
from bauh import ROOT_DIR
from bauh.api.abstract.controller import SoftwareManager
+from bauh.api.abstract.download import FileDownloader
from bauh.api.abstract.view import ViewComponent, TabComponent, InputOption, TextComponent, MultipleSelectComponent, \
PanelComponent, FormComponent, TabGroupComponent, SingleSelectComponent, SelectViewType, TextInputComponent, \
FileChooserComponent
from bauh.view.core import config, timeshift
from bauh.view.core.config import read_config
+from bauh.view.core.downloader import AdaptableFileDownloader
from bauh.view.util import translation
from bauh.view.util.translation import I18n
@@ -20,11 +22,12 @@ from bauh.view.util.translation import I18n
class GenericSettingsManager:
def __init__(self, managers: List[SoftwareManager], working_managers: List[SoftwareManager],
- logger: logging.Logger, i18n: I18n):
+ logger: logging.Logger, i18n: I18n, file_downloader: FileDownloader):
self.i18n = i18n
self.managers = managers
self.working_managers = working_managers
self.logger = logger
+ self.file_downloader = file_downloader
def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent:
tabs = list()
@@ -118,7 +121,22 @@ class GenericSettingsManager:
max_width=default_width,
value=core_config['download']['multithreaded'])
- sub_comps = [FormComponent([select_dmthread, select_trim_up, select_dep_check, input_data_exp, input_icon_exp], spaces=False)]
+ supported_mthread_clients = self.file_downloader.get_supported_multithreaded_clients()
+ mthread_client_opts = [(self.i18n['default'].capitalize(), None, None)]
+ mthread_client_opts.extend(((d, d, None) for d in supported_mthread_clients))
+ current_mthread_client = core_config['download']['multithreaded_client']
+
+ if current_mthread_client not in supported_mthread_clients:
+ current_mthread_client = None
+
+ select_mthread_client = self._gen_select(label=self.i18n['core.config.download.multithreaded_client'],
+ tip=self.i18n['core.config.download.multithreaded_client.tip'],
+ id_="mthread_client",
+ max_width=default_width,
+ opts=mthread_client_opts,
+ value=current_mthread_client)
+
+ sub_comps = [FormComponent([select_dmthread, select_mthread_client, select_trim_up, select_dep_check, input_data_exp, input_icon_exp], spaces=False)]
return TabComponent(self.i18n['core.config.tab.advanced'].capitalize(), PanelComponent(sub_comps), None, 'core.adv')
def _gen_tray_settings(self, core_config: dict, screen_width: int, screen_height: int) -> TabComponent:
@@ -307,6 +325,13 @@ class GenericSettingsManager:
download_mthreaded = adv_form.get_component('down_mthread').get_selected()
core_config['download']['multithreaded'] = download_mthreaded
+ mthread_client = adv_form.get_component('mthread_client').get_selected()
+ core_config['download']['multithreaded_client'] = mthread_client
+
+ if isinstance(self.file_downloader, AdaptableFileDownloader):
+ self.file_downloader.multithread_client = mthread_client
+ self.file_downloader.multithread_enabled = download_mthreaded
+
single_dep_check = adv_form.get_component('dep_check').get_selected()
core_config['system']['single_dependency_checking'] = single_dep_check
diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca
index a0d41a55..982535af 100644
--- a/bauh/view/resources/locale/ca
+++ b/bauh/view/resources/locale/ca
@@ -192,7 +192,9 @@ core.config.backup.upgrade=Before upgrading
core.config.download.icons=Download icons
core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table
core.config.download.multithreaded=Multithreaded download
-core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed
+core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads (faster).
+core.config.download.multithreaded_client=Multi-threaded download tool
+core.config.download.multithreaded_client.tip=The download tool that should be used for multi-threaded downloads. It must be installed on the system for this configuration to work.
core.config.downloads=downloads
core.config.locale.label=language
core.config.mem_cache=Memory storage
diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de
index 1f411847..a0736c4e 100644
--- a/bauh/view/resources/locale/de
+++ b/bauh/view/resources/locale/de
@@ -191,7 +191,9 @@ core.config.backup.upgrade=Before upgrading
core.config.download.icons=Download Icons
core.config.download.icons.tip=Falls aktiviert werden die Anwendungs-Icons in der Tabelle angezeigt
core.config.download.multithreaded=Paralleler Download
-core.config.download.multithreaded.tip=Falls aktiviert werden Downloads parallel (mit mehreren Threads) heruntergeladen. Dazu muss aria installiert sein.
+core.config.download.multithreaded.tip=Falls aktiviert werden Downloads parallel (mit mehreren Threads) heruntergeladen.
+core.config.download.multithreaded_client=Multi-threaded download tool
+core.config.download.multithreaded_client.tip=The download tool that should be used for multi-threaded downloads. It must be installed on the system for this configuration to work.
core.config.downloads=Downloads
core.config.locale.label=Sprache
core.config.mem_cache=RAM Cache
diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en
index 36ab76f8..c1ad3e2c 100644
--- a/bauh/view/resources/locale/en
+++ b/bauh/view/resources/locale/en
@@ -190,8 +190,10 @@ core.config.backup.upgrade=Before upgrading
core.config.backup=Enabled
core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table
core.config.download.icons=Download icons
-core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed
core.config.download.multithreaded=Multi-threaded download
+core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads (faster).
+core.config.download.multithreaded_client=Multi-threaded download tool
+core.config.download.multithreaded_client.tip=The download tool that should be used for multi-threaded downloads. It must be installed on the system for this configuration to work.
core.config.downloads=downloads
core.config.locale.label=language
core.config.mem_cache.data_exp.tip=Defines the in-memory data lifetime ( in SECONDS )
diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es
index 66ee55ef..ae2714ec 100644
--- a/bauh/view/resources/locale/es
+++ b/bauh/view/resources/locale/es
@@ -192,7 +192,9 @@ core.config.backup.upgrade=Antes de actualizar
core.config.download.icons=Descargar iconos
core.config.download.icons.tip=Si los íconos de las aplicaciones se deben descargar y mostrar en la tabla
core.config.download.multithreaded=Descarga segmentada
-core.config.download.multithreaded.tip=Si las aplicaciones, paquetes y archivos deben descargarse con una herramienta que usa segmentación / threads ( más rápido ). Por el momento, esta configuración solo funcionará si el paquete aria2 esté instalado
+core.config.download.multithreaded.tip=Si las aplicaciones, paquetes y archivos deben descargarse con una herramienta que usa segmentación / threads (más rápido).
+core.config.download.multithreaded_client=Herramienta para descarga segmentada
+core.config.download.multithreaded_client.tip=La herramienta que debe usarse para descargas segmentadas. Ella debe estar instalada en el sistema para que esta configuración funcione.
core.config.downloads=downloads
core.config.locale.label=idioma
core.config.mem_cache=Almacenamiento de memoria
diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it
index 3683751b..252a2427 100644
--- a/bauh/view/resources/locale/it
+++ b/bauh/view/resources/locale/it
@@ -191,7 +191,9 @@ core.config.backup.upgrade=Before upgrading
core.config.download.icons=Download icons
core.config.download.icons.tip=If the application icons should be downloaded and displayed on the table
core.config.download.multithreaded=Multithreaded download
-core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads ( faster ). At the moment this setting will only work if the aria2 package is installed
+core.config.download.multithreaded.tip=Whether applications, packages and files should be downloaded with a tool that works with threads (faster).
+core.config.download.multithreaded_client=Multi-threaded download tool
+core.config.download.multithreaded_client.tip=The download tool that should be used for multi-threaded downloads. It must be installed on the system for this configuration to work.
core.config.downloads=downloads
core.config.locale.label=language
core.config.mem_cache=Memory storage
diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt
index eb645cac..82da564c 100644
--- a/bauh/view/resources/locale/pt
+++ b/bauh/view/resources/locale/pt
@@ -190,8 +190,10 @@ core.config.backup.upgrade=Antes de atualizar
core.config.backup=Habilitada
core.config.download.icons.tip=Se os ícones dos aplicativos devem ser baixados e exibidos na tabela
core.config.download.icons=Baixar ícones
-core.config.download.multithreaded.tip=Se os aplicativos, pacotes e arquivos devem ser baixados através de uma ferramenta que trabalha com segmentação / threads ( mais rápido ). No momento esta propriedade somente funcionará se o pacote aria2 estiver instalado.
+core.config.download.multithreaded.tip=Se os aplicativos, pacotes e arquivos devem ser baixados através de uma ferramenta que trabalha com segmentação / threads (mais rápido).
core.config.download.multithreaded=Download segmentado
+core.config.download.multithreaded_client=Ferramenta para download segmentado
+core.config.download.multithreaded_client.tip=A ferramenta que deve ser utilizada para downloads segmentados. Ela precisa estar instalada no sistema para que essa configuração funcione.
core.config.downloads=downloads
core.config.locale.label=idioma
core.config.mem_cache.data_exp.tip=Define o tempo de vida dos dados em memória ( em SEGUNDOS )
diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru
index a04deaed..1794d428 100644
--- a/bauh/view/resources/locale/ru
+++ b/bauh/view/resources/locale/ru
@@ -191,7 +191,9 @@ core.config.backup.upgrade=Before upgrading
core.config.download.icons=Скачать иконки
core.config.download.icons.tip=Загружать иконки приложения для отображаения на рабочем столе
core.config.download.multithreaded=Многопоточная загрузка
-core.config.download.multithreaded.tip=Следует ли загружать приложения, пакеты и файлы с помощью инструмента, который работает с потоками ( быстрее ). На данный момент эта настройка будет работать только в том случае, если установлен пакет aria2
+core.config.download.multithreaded.tip=Следует ли загружать приложения, пакеты и файлы с помощью инструмента, который работает с потоками (быстрее).
+core.config.download.multithreaded_client=Multi-threaded download tool
+core.config.download.multithreaded_client.tip=The download tool that should be used for multi-threaded downloads. It must be installed on the system for this configuration to work.
core.config.downloads=downloads
core.config.locale.label=Язык
core.config.mem_cache=Запоминающее устройство
diff --git a/bauh/view/resources/locale/tr b/bauh/view/resources/locale/tr
index 01e65865..993f9e82 100644
--- a/bauh/view/resources/locale/tr
+++ b/bauh/view/resources/locale/tr
@@ -190,8 +190,10 @@ core.config.backup.upgrade=Önce yükseltiliyor
core.config.backup=Etkin
core.config.download.icons.tip=Tabloda görüntüleniyorsa uygulama simgeleri indirilmeli
core.config.download.icons=Simgeleri indir
-core.config.download.multithreaded.tip=Uygulamaların, paketlerin ve dosyaların iş parçacıklarıyla (daha hızlı) çalışan bir araçla indirilip indirilmeyeceği. Şu anda bu ayar yalnızca aria2 paketi kuruluysa çalışır
+core.config.download.multithreaded.tip=Uygulamaların, paketlerin ve dosyaların iş parçacıklarıyla (daha hızlı) çalışan bir araçla indirilip indirilmeyeceği.
core.config.download.multithreaded=Çoklu parçalı indirme
+core.config.download.multithreaded_client=Multi-threaded download tool
+core.config.download.multithreaded_client.tip=The download tool that should be used for multi-threaded downloads. It must be installed on the system for this configuration to work.
core.config.downloads=indirilenler
core.config.locale.label=dil
core.config.mem_cache.data_exp.tip=Bellek içi veri ömrünü tanımlar ( SANİYE )
diff --git a/pictures/releases/0.9.4/mthread_tool.png b/pictures/releases/0.9.4/mthread_tool.png
new file mode 100644
index 0000000000000000000000000000000000000000..99c2f32988ca65d3c5766d055e3e4b47e3645436
GIT binary patch
literal 16585
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