Skip to content

Add code to link remote and local paths. #212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'pyxdg',
'blinker',
'natsort',
'bidict',
],
extras_require = {
'setproctitle': ['setproctitle'],
Expand Down
48 changes: 48 additions & 0 deletions stig/commands/base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,51 @@ def completion_candidates_posargs(cls, args):
return candidates.Candidates(cands, label='Direction', curarg_seps=(',',))
elif posargs.curarg_index >= 3:
return candidates.torrent_filter(args.curarg)

class LinkPathCmd(metaclass=CommandMeta):
name = 'link'
aliases = ()
category = 'configuration'
provides = set()
description = textwrap.dedent(
"""Links remote and local paths. Call with no arguments to list existing links.

Example use case: a remote daemon downloads to
/var/transmission/downloads/, but this is mounted on the local client
as /mnt/nfs/seedbox/. Moving torrents with the move requires the remote
path, but tab completion works with the local filesystem. Path links
provide a transparent translation layer.

Links must be bijective; as a consequence the --force flag must be
passed to ensure that clobbering is intentional.
""")
usage = ('link [<OPTIONS>] REMOTE LOCAL',)
examples = ('link /var/transmission/downloads/ /mnt/nfs/seedbox/',
'link')
argspecs = (
{'names': ('--force', '-f'),
'description': 'Overwrite existing links involving REMOTE or LOCAL.'},
{'names': ('REMOTE',),
'nargs': '?',
'description': 'Remote path to link'},
{'names': ('LOCAL',),
'nargs': '?',
'description': 'Local path to link'}
)

DUMP_WIDTH = 79

async def run(self, REMOTE, LOCAL, force):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason this needs to be async? Every nested scope this coroutine would generate appears to use blocking methods.

log.debug("%s %s %s" % (REMOTE, LOCAL, force))
if not (REMOTE or LOCAL):
log.debug('Listing links')
for link in objects.pathtranslator.links.items():
self.info('%s <-> %s' % link)
return
elif not (REMOTE and LOCAL):
self.error("Must specify both REMOTE and LOCAL paths.")
return
try:
objects.pathtranslator.link(REMOTE, LOCAL, force)
except Exception as e:
raise CmdError(e)
4 changes: 4 additions & 0 deletions stig/commands/base/torrent.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import asyncio
import os
from pathlib import Path

from . import _mixin as mixin
from .. import CmdError, CommandMeta
Expand Down Expand Up @@ -48,6 +49,8 @@ class AddTorrentsCmdbase(metaclass=CommandMeta):
async def run(self, TORRENT, stopped, path):
success = True
force_torrentlist_update = False
if path:
path = objects.pathtranslator.to_remote(path)
for source in TORRENT:
source_abs_path = self.make_path_absolute(source)
response = await self.make_request(objects.srvapi.torrent.add(source_abs_path,
Expand Down Expand Up @@ -250,6 +253,7 @@ async def run(self, TORRENT_FILTER, PATH):
except ValueError as e:
raise CmdError(e)
else:
PATH = objects.pathtranslator.to_remote(Path(PATH)).as_posix()
response = await self.make_request(objects.srvapi.torrent.move(tfilter, PATH),
polling_frenzy=True)
if not response.success:
Expand Down
3 changes: 3 additions & 0 deletions stig/commands/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,6 @@ async def _show_limits(self, TORRENT_FILTER, directions):

def _output(self, msg):
print(msg)

class LinkPathCmd(base.LinkPathCmd):
provides = {'cli'}
3 changes: 3 additions & 0 deletions stig/commands/tui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ async def _show_limits(self, TORRENT_FILTER, directions):

def _output(self, msg):
self.info(msg)

class LinkPathCmd(base.LinkPathCmd):
provides = {'tui'}
2 changes: 2 additions & 0 deletions stig/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .client import API
from .commands import CommandManager
from .helpmgr import HelpManager
from .utils._pathtranslator import PathTranslator

log = logging.make_logger()

Expand All @@ -26,6 +27,7 @@

localcfg = settings.Settings()
settings.init_defaults(localcfg)
pathtranslator = PathTranslator()

srvapi = API(interval=localcfg['tui.poll'])

Expand Down
66 changes: 66 additions & 0 deletions stig/utils/_pathtranslator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details
# http://www.gnu.org/licenses/gpl-3.0.txt

from pathlib import Path
from bidict import bidict, ValueDuplicationError

from ..logging import make_logger # isort:skip

log = make_logger(__name__)


class PathTranslator:
def __init__(self):
# links: a dict of (remote_root, local_root) pairs
self.links = bidict()

def link(self, remote, local, force=False):
remote = Path(remote)
local = Path(local)
if not local.exists():
raise ValueError("Local path %s does not exist." % local)
if not local.is_absolute():
raise ValueError("Local path %s must be absolute." % local)
if not remote.is_absolute():
raise ValueError("Remote path %s must be absolute." % remote)
if force:
self.links.forceput(remote, local)
if remote in self.links.keys():
raise ValueError(
"Remote path %s is already linked to local path %s."
% (remote, self.links[remote])
)
try:
self.links[remote] = local
except ValueDuplicationError:
raise ValueError(
"Local path %s is already linked to remote path %s."
% (local, self.links[remote])
)
log.debug("Linked remote path %s to local path %s" % (remote, local))

def _translate_path(self, path, links):
try:
path = Path(path)
except Exception:
raise ValueError("Could not convert %s to Path" % path)
for roots in links.items():
try:
return roots[1] / path.relative_to(roots[0])
except ValueError:
pass
return path

def to_local(self, p_rem):
return self._translate_path(p_rem, self.links)

def to_remote(self, p_loc):
return self._translate_path(p_loc, self.links.inv)