From 2e014f141d209485a10e17bc4a15913610964e33 Mon Sep 17 00:00:00 2001 From: leafee98 Date: Sun, 21 Sep 2025 16:59:47 +0800 Subject: [PATCH] Basic logic finished --- README | 4 ++ link-tool.py | 106 ++++++++++++++++++++++++++++++++++++++++++++ test/src/file1.txt | 0 test/src/link1.conf | 7 +++ 4 files changed, 117 insertions(+) create mode 100644 README create mode 100644 link-tool.py create mode 100644 test/src/file1.txt create mode 100644 test/src/link1.conf diff --git a/README b/README new file mode 100644 index 0000000..9c2f239 --- /dev/null +++ b/README @@ -0,0 +1,4 @@ +# Link tool + +This tools is used to quickly (re-)create hard link. which is useful +when creating link for jellyfin from qbittorrent. diff --git a/link-tool.py b/link-tool.py new file mode 100644 index 0000000..312fe5e --- /dev/null +++ b/link-tool.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +import argparse +import dataclasses +import os +import logging + +from typing import List, Tuple, Union + + +logger = logging.getLogger(__name__) + +@dataclasses.dataclass(repr=True) +class Link: + src: str + dst: str + +@dataclasses.dataclass(repr=True) +class Conf: + cwd: Union[str, None] + + def assign(self, var_name, var_value): + if hasattr(self, var_name): + logger.debug('assign config: {} = {}'.format(var_name, var_value)) + setattr(self, var_name, var_value) + else: + raise Exception('Unknown config: {}'.format(var_name)) + + def validate(self): + missing_fields = [] + if self.cwd is None: + missing_fields.append('cwd') + if len(missing_fields) > 0: + logger.error('there are missing field: {}'.format(missing_fields)) + return len(missing_fields) == 0 + +def load_config(conf_path: str) -> Tuple[Conf, List[Link]]: + with open(conf_path, 'r') as f: + lines = f.readlines() + + lines = [ line.strip() for line in lines ] + lines = [ line for line in lines if len(line) > 0 ] + lines = [ line for line in lines if not line.startswith('#') ] + + links = [] + conf = Conf(cwd=None) + for line in lines: + if '=' in line: + line_fields = line.split('=') + logger.debug('[config ] line fields: {}'.format(line_fields)) + var_name = line_fields[0].strip() + var_value = line_fields[1].strip() + + conf.assign(var_name, var_value) + + elif '->' in line: + line_fields = line.split('->') + logger.debug('[link-rule] line fields: {}'.format(line_fields)) + src = line_fields[0].strip() + dst = line_fields[1].strip() + + links.append(Link(src, dst)) + + else: + raise Exception('Unknown config line: {}'.format(line)) + + return conf, links + +def make_link(links: List[Link]): + for link in links: + if os.path.exists(link.dst): + logger.warning('dst exists, removing: {}'.format(link.dst)) + os.remove(link.dst) + + dst_dir = os.path.dirname(link.dst) + if not os.path.exists(dst_dir): + logger.info('dst directory not exists, creating: {}'.format(dst_dir)) + os.makedirs(dst_dir) + + os.link(link.src, link.dst) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', required=True, help='configuration file') + parser.add_argument('-v', '--verbose', action='store_true' ,help='more verbose log') + + args = parser.parse_args() + + config_path = args.config + verbose = args.verbose + + logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO) + + config, links = load_config(config_path) + + if not config.validate(): + logger.error('invalid configuration, exiting...') + exit(1) + + for link in links: + logger.info('loaded link: {}'.format(link)) + + make_link(links) + +if __name__ == '__main__': + main() diff --git a/test/src/file1.txt b/test/src/file1.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/src/link1.conf b/test/src/link1.conf new file mode 100644 index 0000000..2142be8 --- /dev/null +++ b/test/src/link1.conf @@ -0,0 +1,7 @@ +# cwd is used for the pwd of src and dst defined below. +# the cwd is relative to the its config file, such as this file +cwd = '.' + +# this is comment +test/src/file1.txt -> test/dst/file1.txt +