class Node(object): """ General node. Inherited by header nodes or entry nodes. """ State = Enum('NodeState', 'NORMAL, PICKED, UNDEROP') ToggleOpRes = Enum('NodeToggleOpRes', 'INVALID, ON, OFF') def __init__(self, fullpath, name, highlight, level=0): self.fullpath = fullpath self.name = name self.set_highlight(highlight) self.level = level self.state = Node.State.NORMAL self.is_cursor_on = False def set_highlight(self, highlight): if type(highlight) is str: highlight = colortbl[highlight] self.highlight = highlight @property def highlight_content(self): return c256(self.name, self.highlight, self.is_cursor_on) @property def isDir(self): return False @property def isINFO(self): return False def cursor_on(self): self.is_cursor_on = True def cursor_off(self): self.is_cursor_on = False def toggle_pick(self): return Node.ToggleOpRes.INVALID def re_stat(self, fs): pass
class Node(object): """ General node. Inherited by header nodes or entry nodes. """ State = Enum('NodeState', 'NORMAL, PICKED, UNDEROP') ToggleOpRes = Enum('NodeToggleOpRes', 'INVALID, ON, OFF') def __init__(self, fullpath, name, highlight, level=0): self.fullpath = fullpath self.name = name self.set_highlight(highlight) self.level = level self.state = Node.State.NORMAL def set_highlight(self, highlight, cursor_on=False): if type(highlight) is str: highlight = colortbl[highlight] if cursor_on: self.highlight = '[38;5;{};7'.format(highlight) else: self.highlight = '[38;5;{}'.format(highlight) @property def highlight_content(self): return '{}m{}[0m'.format(self.highlight, self.name) @property def isDir(self): return False @property def isHeader(self): return False def cursor_on(self): pass def cursor_off(self): pass def toggle_pick(self): return Node.ToggleOpRes.INVALID
class Rclone(LocalFS): ServerCmd = util.GenNetRangerScriptCmd('rclone_server') is_remote = True SyncDirection = Enum('SyncDirection', 'DOWN, UP') @classmethod def sync_src_dst(self, lpath, direction): rpath = self.rpath(lpath) if direction == Rclone.SyncDirection.UP: return lpath, rpath else: return rpath, lpath @classmethod def init(self, root_dir, remote_remap): self._flags = Vim.Var("_NETRRcloneFlags", default="") self.rclone_rcd_port = Vim.Var('NETRcloneRcdPort') if root_dir[-1] == '/': root_dir = root_dir[:-1] self.root_dir = root_dir self.rplen = len(root_dir) + 1 self.rcd_started = False for remote, root in remote_remap.items(): if root[-1] != '/': remote_remap[remote] += '/' self.remote_remap = remote_remap Shell.mkdir(root_dir) @classmethod def is_remote_path(self, path): return path.startswith(self.root_dir) @classmethod def rpath(self, lpath): if not lpath.startswith(self.root_dir): return lpath rpath = lpath[self.rplen:] if '/' not in rpath: rpath += ':/' else: rpath = rpath.replace('/', ':/', 1) for remote, root in self.remote_remap.items(): if rpath.startswith(remote): rpath = rpath.replace('/', root, 1) return rpath @classmethod def ls(self, dirname, cheap_remote_ls=False): if not cheap_remote_ls and len(dirname) > len(self.root_dir): local_files = set([ name for name in Shell.run(f'ls -p "{dirname}"').split('\n') if len(name) > 0 ]) remote_files = set([ name for name in Shell.run( f'rclone {self._flags} lsf "{self.rpath(dirname)}"').split( '\n') if len(name) > 0 ]) for name in remote_files.difference(local_files): if name[-1] == '/': Shell.mkdir(os.path.join(dirname, name)) else: Shell.touch(os.path.join(dirname, name)) for name in local_files.difference(remote_files): # TODO use Vim.AsyncRun instead Shell.run_async(f'rclone {self._flags} copyto --tpslimit=10 \ "{os.path.join(dirname, name)}" \ "{os.path.join(self.rpath(dirname), name)}"') return super(Rclone, self).ls(dirname) @classmethod def ensure_remote_downloaded(self, lpath): if os.stat(lpath).st_size == 0: src, dst = self.sync_src_dst(lpath, Rclone.SyncDirection.DOWN) Shell.run( f'rclone {self._flags} copyto --tpslimit=10 "{src}" "{dst}"') @classmethod def rename(self, src, dst): Shell.run( f'rclone {self._flags} moveto "{self.rpath(src)}" "{self.rpath(dst)}"' ) super(Rclone, self).rename(src, dst) @classmethod def mv(self, src_arr, dst, on_exit): self.exec_server_cmd( 'mv', on_exit, { 'rsrc': [self.rpath(s) for s in src_arr], 'src': src_arr, 'rdst': self.rpath(dst), 'dst': dst, 'flags': self._flags }) @classmethod def cp(self, src_arr, dst, on_exit): self.exec_server_cmd( 'cp', on_exit, { 'rsrc': [self.rpath(s) for s in src_arr], 'src': src_arr, 'rdst': self.rpath(dst), 'dst': dst, 'flags': self._flags }) @classmethod def rm(self, src_arr, force=False, on_exit=None): self.exec_server_cmd( 'rm', on_exit, { 'rsrc': [self.rpath(s) for s in src_arr], 'src': src_arr, 'flags': self._flags }) @classmethod def touch(self, name): Shell.touch(name) @classmethod def mkdir(self, name): Shell.mkdir(name) @classmethod def sync(self, lpath, direction): src, dst = self.sync_src_dst(lpath, direction) Shell.run(f'rclone {self._flags} sync "{src}" "{dst}"') @classmethod def run_cmd(self, cmd): if not self.rcd_started: Vim.AsyncRun( f'rclone {self._flags} rcd --rc-no-auth --rc-addr=localhost:{self.rclone_rcd_port}' ) self.rcd_started = True # Ensure the server running before executing the next command. time.sleep(.1) return json.loads( Shell.run( f'rclone {self._flags} rc --rc-addr=localhost:{self.rclone_rcd_port} {cmd}' )) @classmethod def cmd_listremotes(self): return self.run_cmd('config/listremotes')['remotes'] @classmethod def list_remotes_in_vim_buffer(self): if not Shell.isinPATH('rclone'): self.install_rclone() remotes = set(self.cmd_listremotes()) local_remotes = set(super(Rclone, self).ls(self.root_dir)) for remote in remotes.difference(local_remotes): Shell.mkdir(os.path.join(self.root_dir, remote)) for remote in local_remotes.difference(remotes): Shell.rm(os.path.join(self.root_dir, remote)) if len(remotes) > 0: Vim.command(f'NETRTabdrop {self.root_dir}') else: Vim.ErrorMsg( "There's no remote now. Run 'rclone config' in a terminal to " "setup remotes and restart vim again.") @classmethod def install_rclone(self): import platform import zipfile rclone_dir = Vim.UserInput( 'Rclone not in PATH. Install it at (modify/enter)', os.path.expanduser('~/rclone')) Shell.mkdir(rclone_dir) system = platform.system().lower() processor = 'amd64' if '386' in platform.processor(): processor = '386' else: # Should support arm?? pass url = f'https://downloads.rclone.org/rclone-current-{system}-{processor}.zip' zip_fname = os.path.join(rclone_dir, 'rclone.zip') Shell.urldownload(url, zip_fname) zip_ref = zipfile.ZipFile(zip_fname, 'r') zip_ref.extractall(rclone_dir) for entry in zip_ref.NameToInfo: if entry.endswith('rclone'): Shell.cp(os.path.join(rclone_dir, entry), rclone_dir) Shell.chmod(os.path.join(rclone_dir, 'rclone'), 755) zip_ref.close() os.remove(zip_fname) shellrc = Vim.UserInput( 'Update PATH in (leave blank to set manually later)', Shell.shellrc()) if len(shellrc) > 0: with open(shellrc, 'a') as f: f.write(f'PATH={rclone_dir}:$PATH\n') os.environ['PATH'] += ':' + rclone_dir
from __future__ import absolute_import import json import os import pickle import re import shutil import tempfile import time from netranger import Vim, util from netranger.config import file_sz_display_wid from netranger.enum import Enum from netranger.shell import Shell FType = Enum('FileType', 'SOCK, LNK, REG, BLK, DIR, CHR, FIFO') def do_nothing(): pass class FSTarget(object): def __init__(self, target_path=''): """ This is a help class for separating local files and remote files for mv, cp, rm commands. Though the logic in this class can be done in the caller side, it makes the caller has higher branch number and hard-to-read code. """ self.remote_targets = [] self.local_targets = []
class Repo(object): State = Enum( 'GitState', 'INVALID, IGNORED, UNTRACKED, UNMODIFIED, MODIFIED, STAGED, ' 'STAGEDMODIFIED, UNMERGED') def __init__(self, path): self.path = path self.path_len = len(path) + 1 self.staged_str = None self.modified_str = None self.ignored_str = None self.commit_edit_msg = os.path.join(self.path, '.git/COMMIT_EDITMSG') def run_cmd(self, cmd): return Shell.run('git -C {} {}'.format(self.path, cmd)) def get_state(self, fullpath): rel_path = fullpath[self.path_len:] if os.path.isdir(fullpath): if not rel_path: return Repo.State.INVALID if self.staged_str is None: self.staged_str = self.run_cmd('diff --name-only --cached') self.modified_str = self.run_cmd('ls-files -m') self.ignored_str = self.run_cmd( 'ls-files --others -i --exclude-standard') if re.search(rel_path, self.staged_str): if re.search(rel_path, self.modified_str): return Repo.State.STAGEDMODIFIED else: return Repo.State.STAGED elif re.search(rel_path, self.modified_str): return Repo.State.MODIFIED elif re.search(rel_path + '\n', self.ignored_str): return Repo.State.IGNORED else: return Repo.State.INVALID else: state_str = self.run_cmd( 'status --porcelain --ignored -uall {}'.format(rel_path)) if state_str: return { "!!": Repo.State.IGNORED, '??': Repo.State.UNTRACKED, ' M': Repo.State.MODIFIED, 'MM': Repo.State.STAGEDMODIFIED, 'M ': Repo.State.STAGED }[state_str[:2]] else: return Repo.State.INVALID def get_prev_and_next_state(self, fullpath): cur_state = self.get_state(fullpath) if cur_state == Repo.State.UNTRACKED: return Repo.State.INVALID, Repo.State.STAGED elif (cur_state == Repo.State.STAGEDMODIFIED or cur_state == Repo.State.STAGED): return Repo.State.MODIFIED, Repo.State.STAGED elif cur_state == Repo.State.MODIFIED: return Repo.State.UNMODIFIED, Repo.State.STAGED elif cur_state == Repo.State.IGNORED: return Repo.State.INVALID, Repo.State.STAGED elif cur_state == Repo.State.INVALID: return Repo.State.INVALID, Repo.State.INVALID else: assert False, "get_prev_and_next_state: " "Unhandled case: " + cur_state def stage(self, fullpath): self.run_cmd('add {}'.format(fullpath)) def unstage(self, fullpath): self.run_cmd('reset HEAD {}'.format(fullpath)) def unmodify(self, fullpath): ans = Vim.UserInput("This will discard any made changes. Proceed " "anyway? (y/n)") if ans == 'y': self.run_cmd('checkout {}'.format(fullpath)) def commit(self, amend=False): if amend: # TODO Should we check if already pushed? return self.run_cmd('commit --amend --no-edit') else: with open(self.commit_edit_msg, 'w') as file: lines = [] for line in file: line = line.strip() if line and line[0] != '#': lines.append(line) if len(lines) > 0: return self.run_cmd('commit -m "{}"'.format( ''.join(lines))) def stage_file_content(self, fullpath): rel_path = fullpath[self.path_len:] return self.run_cmd('cat-file -p :{}'.format(rel_path))
class Rclone(FS): SyncDirection = Enum('SyncDirection', 'DOWN, UP') def sync_src_dst(self, lpath, direction): rpath = self.rpath(lpath) if direction == Rclone.SyncDirection.UP: return lpath, rpath else: return rpath, lpath def __init__(self, root_dir, remote_remap): if root_dir[-1] == '/': root_dir = root_dir[:-1] self.root_dir = root_dir self.rplen = len(root_dir) + 1 for remote, root in remote_remap.items(): if root[-1] != '/': remote_remap[remote] += '/' self.remote_remap = remote_remap Shell.mkdir(root_dir) remotes = set(Shell.run('rclone listremotes').split(':\n')) local_remotes = set(super(Rclone, self).ls(root_dir)) for remote in remotes.difference(local_remotes): Shell.mkdir(os.path.join(root_dir, remote)) for remote in local_remotes.difference(remotes): super(Rclone, self).rm(os.path.join(root_dir, remote), True) self.has_remote = len(remotes) > 0 self.ls_time_stamp = {} def isRemotePath(self, lpath): return lpath.startswith(self.root_dir) def rpath(self, lpath): if not lpath.startswith(self.root_dir): return lpath rpath = lpath[self.rplen:] if '/' not in rpath: rpath += ':/' else: rpath = rpath.replace('/', ':/', 1) for remote, root in self.remote_remap.items(): if rpath.startswith(remote): rpath = rpath.replace('/', root, 1) return rpath def ls(self, dirname, cheap_remote_ls=False): if not cheap_remote_ls and len(dirname) > len(self.root_dir): local_files = set([ name for name in Shell.run('ls -p {}'.format(dirname)).split('\n') if len(name) > 0 ]) remote_files = set([ name for name in Shell.run('rclone lsf "{}"'.format( self.rpath(dirname))).split('\n') if len(name) > 0 ]) for name in remote_files.difference(local_files): if name[-1] == '/': Shell.mkdir(os.path.join(dirname, name)) else: Shell.touch(os.path.join(dirname, name)) for name in local_files.difference(remote_files): log('rclone copyto "{}" "{}"'.format( os.path.join(dirname, name), os.path.join(self.rpath(dirname), name))) Shell.run('rclone copyto "{}" "{}"'.format( os.path.join(dirname, name), os.path.join(self.rpath(dirname), name))) return super(Rclone, self).ls(dirname) def ensure_downloaded(self, lpath): if os.stat(lpath).st_size == 0: src, dst = self.sync_src_dst(lpath, Rclone.SyncDirection.DOWN) Shell.run('rclone copyto "{}" "{}"'.format(src, dst)) def rename(self, src, dst): Shell.run('rclone moveto "{}" "{}"'.format(self.rpath(src), self.rpath(dst))) super(Rclone, self).rename(src, dst) def mv(self, src, dst): # TODO could be more efficient without calling rclone two times self.cp(src, dst) self.rm(src) def cp(self, src, dst): Shell.run('rclone copyto "{}" "{}"'.format( self.rpath(src), os.path.join(self.rpath(dst), os.path.basename(src)))) super(Rclone, self).cp(src, dst) def rm(self, target, force=False): cmd = 'purge' if os.path.isdir(target) else 'delete' Shell.run('rclone {} "{}"'.format(cmd, self.rpath(target))) super(Rclone, self).rm(target, force=True) def sync(self, lpath, direction): src, dst = self.sync_src_dst(lpath, direction) Shell.run('rclone sync "{}" "{}"'.format(src, dst)) def parent_dir(self, cwd): if len(cwd) == self.rplen - 1: return cwd return os.path.abspath(os.path.join(cwd, os.pardir)) @classmethod def valid_or_install(cls, vim): import platform import zipfile if Shell.isinPATH('rclone'): return True else: rclone_dir = VimUserInput( 'Rclone not in PATH. Install it at (modify/enter)', os.path.expanduser('~/rclone')) Shell.mkdir(rclone_dir) system = platform.system().lower() processor = 'amd64' if '386' in platform.processor(): processor = '386' else: # Should support arm?? pass url = 'https://downloads.rclone.org/rclone-current-{}-{}.zip'.format( system, processor) zip_fname = os.path.join(rclone_dir, 'rclone.zip') Shell.urldownload(url, zip_fname) zip_ref = zipfile.ZipFile(zip_fname, 'r') zip_ref.extractall(rclone_dir) for entry in zip_ref.NameToInfo: if entry.endswith('rclone'): Shell.cp(os.path.join(rclone_dir, entry), rclone_dir) Shell.chmod(os.path.join(rclone_dir, 'rclone'), 755) zip_ref.close() os.remove(zip_fname) shellrc = VimUserInput( 'Update PATH in (leave blank to set manually later)', Shell.shellrc()) if len(shellrc) > 0: with open(shellrc, 'a') as f: f.write('PATH={}:$PATH\n'.format(rclone_dir)) os.environ['PATH'] += ':' + rclone_dir
class Rclone(FS): FScmds = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../rclone_server.py') SyncDirection = Enum('SyncDirection', 'DOWN, UP') def sync_src_dst(self, lpath, direction): rpath = self.rpath(lpath) if direction == Rclone.SyncDirection.UP: return lpath, rpath else: return rpath, lpath def __init__(self, root_dir, remote_remap): if root_dir[-1] == '/': root_dir = root_dir[:-1] self.root_dir = root_dir self.rplen = len(root_dir) + 1 for remote, root in remote_remap.items(): if root[-1] != '/': remote_remap[remote] += '/' self.remote_remap = remote_remap Shell.mkdir(root_dir) remotes = set([ line for line in Shell.run('rclone listremotes').split(':\n') if line ]) local_remotes = set(super(Rclone, self).ls(root_dir)) for remote in remotes.difference(local_remotes): Shell.mkdir(os.path.join(root_dir, remote)) for remote in local_remotes.difference(remotes): Shell.rm(os.path.join(root_dir, remote)) self.has_remote = len(remotes) > 0 self.ls_time_stamp = {} def rpath(self, lpath): if not lpath.startswith(self.root_dir): return lpath rpath = lpath[self.rplen:] if '/' not in rpath: rpath += ':/' else: rpath = rpath.replace('/', ':/', 1) for remote, root in self.remote_remap.items(): if rpath.startswith(remote): rpath = rpath.replace('/', root, 1) return rpath def ls(self, dirname, cheap_remote_ls=False): if not cheap_remote_ls and len(dirname) > len(self.root_dir): local_files = set([ name for name in Shell.run('ls -p {}'.format(dirname)).split('\n') if len(name) > 0 ]) remote_files = set([ name for name in Shell.run('rclone lsf "{}"'.format( self.rpath(dirname))).split('\n') if len(name) > 0 ]) for name in remote_files.difference(local_files): if name[-1] == '/': Shell.mkdir(os.path.join(dirname, name)) else: Shell.touch(os.path.join(dirname, name)) for name in local_files.difference(remote_files): Shell.run('rclone copyto --tpslimit=10 "{}" "{}"'.format( os.path.join(dirname, name), os.path.join(self.rpath(dirname), name))) return super(Rclone, self).ls(dirname) def ensure_downloaded(self, lpath): if os.stat(lpath).st_size == 0: src, dst = self.sync_src_dst(lpath, Rclone.SyncDirection.DOWN) Shell.run('rclone copyto --tpslimit=10 "{}" "{}"'.format(src, dst)) def rename(self, src, dst): Shell.run('rclone moveto "{}" "{}"'.format(self.rpath(src), self.rpath(dst))) super(Rclone, self).rename(src, dst) def exec_rclone_server_cmd(self, cmd, on_exit, arguments): fname = tempfile.mkstemp()[1] with open(fname, 'ab') as f: pickle.dump(arguments, f) Shell.run_async('python {} {} {}'.format(Rclone.FScmds, cmd, fname), on_exit=on_exit) def mv(self, src_arr, dst, on_exit): self.exec_rclone_server_cmd( 'mv', on_exit, { 'rsrc': [self.rpath(s) for s in src_arr], 'src': src_arr, 'rdst': self.rpath(dst), 'dst': dst }) def cp(self, src_arr, dst, on_exit): self.exec_rclone_server_cmd( 'cp', on_exit, { 'rsrc': [self.rpath(s) for s in src_arr], 'src': src_arr, 'rdst': self.rpath(dst), 'dst': dst }) def rm(self, src_arr, force=False, on_exit=None): self.exec_rclone_server_cmd('rm', on_exit, { 'rsrc': [self.rpath(s) for s in src_arr], 'src': src_arr }) def touch(self, name): Shell.touch(name) Shell.run('rclone copyto "{}" "{}"'.format( name, os.path.join(self.rpath(name), os.path.basename(name)))) def mkdir(self, name): Shell.mkdir(name) def sync(self, lpath, direction): src, dst = self.sync_src_dst(lpath, direction) Shell.run('rclone sync "{}" "{}"'.format(src, dst)) def parent_dir(self, cwd): if len(cwd) == self.rplen - 1: return cwd return os.path.abspath(os.path.join(cwd, os.pardir)) @classmethod def valid_or_install(cls, vim): import platform import zipfile if Shell.isinPATH('rclone'): return True else: rclone_dir = VimUserInput( 'Rclone not in PATH. Install it at (modify/enter)', os.path.expanduser('~/rclone')) Shell.mkdir(rclone_dir) system = platform.system().lower() processor = 'amd64' if '386' in platform.processor(): processor = '386' else: # Should support arm?? pass url = 'https://downloads.rclone.org/rclone-current-{}-{}.zip'\ .format(system, processor) zip_fname = os.path.join(rclone_dir, 'rclone.zip') Shell.urldownload(url, zip_fname) zip_ref = zipfile.ZipFile(zip_fname, 'r') zip_ref.extractall(rclone_dir) for entry in zip_ref.NameToInfo: if entry.endswith('rclone'): Shell.cp(os.path.join(rclone_dir, entry), rclone_dir) Shell.chmod(os.path.join(rclone_dir, 'rclone'), 755) zip_ref.close() os.remove(zip_fname) shellrc = VimUserInput( 'Update PATH in (leave blank to set manually later)', Shell.shellrc()) if len(shellrc) > 0: with open(shellrc, 'a') as f: f.write('PATH={}:$PATH\n'.format(rclone_dir)) os.environ['PATH'] += ':' + rclone_dir