├── .gitignore ├── .travis.yml ├── README.rst ├── docs ├── HISTORY.rst └── LICENSE ├── setup.py └── src └── tarman ├── __init__.py ├── constants.py ├── containers.py ├── exceptions.py ├── helpers.py ├── overlaywin.py ├── tests ├── __init__.py ├── test_containers.py ├── test_helpers.py ├── test_tree.py └── testdata │ ├── corrupted.tar │ ├── testdata.tar.gz │ ├── testdata │ ├── a │ │ ├── aa │ │ │ └── aaa │ │ ├── ab │ │ │ └── .abb │ │ └── ac │ ├── b │ │ └── ba │ │ │ └── baa │ │ │ ├── baaa │ │ │ └── baaaa │ │ │ └── baab │ └── c │ └── tešt.tar ├── tree.py └── viewarea.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | bin 4 | lib 5 | lib64 6 | 7 | examples/ 8 | include/ 9 | local/ 10 | 11 | !testdata.tar.gz 12 | !tešt.tar 13 | !corrupted.tar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | before_install: 6 | - sudo apt-get update -qq 7 | - sudo apt-get install -qq libarchive12 libarchive-dev 8 | - sudo ln -s /usr/lib/x86_64-linux-gnu/libarchive.so /usr/lib/x86_64-linux-gnu/libarchive.so.13.1.2 9 | install: 10 | - "python setup.py develop" 11 | script: 12 | - "python setup.py test" 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | TarMan 2 | ====== 3 | 4 | 5 | This is archive manager with curses interface. 6 | Have you ever wanted to create an archive quickly without 7 | using help pages in the process for all the command line switches? 8 | Well here comes the tarman, it has only two command line options: 9 | 10 | * --help or 11 | * path to archive or directory 12 | 13 | 14 | At first it was meant to be only for tar archives (hence the name), 15 | but support for other archives does not hurt. 16 | Dependencies are: 17 | 18 | * Python 3 19 | 20 | 21 | It supports archives that are manageable with libarchive. 22 | 23 | For now it supports: 24 | 25 | * file browser 26 | * one-level browsing of supported archives 27 | * extraction of files 28 | * create archive option 29 | 30 | 31 | Install from PYPI 32 | ================= 33 | 34 | .. sourcecode:: bash 35 | 36 | pip install tarman 37 | 38 | 39 | Usage 40 | ===== 41 | 42 | .. sourcecode:: bash 43 | 44 | bin/tarman some/directory/ 45 | 46 | 47 | Key bindings 48 | ============ 49 | 50 | Key bindings are listed in HELP window, 51 | you can access it by pressing *h* or *?* key. 52 | 53 | {help_string} 54 | 55 | 56 | Install of development version 57 | ============================== 58 | 59 | It is very recommended to install it to virtualenv. 60 | 61 | .. sourcecode:: bash 62 | 63 | git clone git://github.com/matejc/tarman.git 64 | virtualenv --no-site-packages tarman 65 | cd tarman/ 66 | bin/pip install . 67 | 68 | 69 | Development 70 | =========== 71 | 72 | .. sourcecode:: bash 73 | 74 | git clone git://github.com/matejc/tarman.git 75 | virtualenv --no-site-packages tarman 76 | cd tarman/ 77 | bin/python setup.py develop 78 | 79 | bin/python setup.py test # to run tests 80 | 81 | -------------------------------------------------------------------------------- /docs/HISTORY.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.1.4 (unreleased) 5 | ------------------ 6 | 7 | - Refactor unicode support for lower level functions. 8 | [Matej Cotman] 9 | - Fixed Hydra build error. 10 | [Matej Cotman] 11 | - Added hidden file view toggle 12 | [Shaun Marshall] 13 | 14 | 15 | 0.1.3 (2013-08-28) 16 | ------------------ 17 | 18 | - Fix crash on permission error 19 | [Matej Cotman] 20 | - Fix some more unicode issues 21 | [Matej Cotman] 22 | - Logging to ~/.tarman.log 23 | [Matej Cotman] 24 | 25 | 26 | 0.1.2 (2013-08-13) 27 | ------------------ 28 | 29 | - Fix corrupted file name crash - ignore those files 30 | [Matej Cotman] 31 | - Switch to python-libarchive lower level api for better control 32 | [Matej Cotman] 33 | 34 | 35 | 0.1.1 (2013-06-18) 36 | ------------------ 37 | 38 | - Fix test failures 39 | [Matej Cotman] 40 | - Fixed unicode issues 41 | [Matej Cotman] 42 | - Add tests for unicode file names 43 | [Matej Cotman] 44 | - Re-organize readme 45 | [Matej Cotman] 46 | - Additional classifiers 47 | [Matej Cotman] 48 | 49 | 50 | 0.1 (2013-06-17) 51 | ---------------- 52 | 53 | - Initial release 54 | [Matej Cotman] 55 | 56 | -------------------------------------------------------------------------------- /docs/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | ==================== 3 | 4 | 5 | Copyright (c) 2013, Matej Cotman 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Installer for this package.""" 3 | import os 4 | import sys 5 | 6 | from setuptools import setup 7 | from setuptools import find_packages 8 | 9 | 10 | def import_path(fullpath): 11 | """ 12 | Import a file with full path specification. Allows one to 13 | import from anywhere, something __import__ does not do. 14 | """ 15 | path, filename = os.path.split(fullpath) 16 | filename, _ = os.path.splitext(filename) 17 | sys.path.append(path) 18 | module = __import__(filename) 19 | # reload(module) # Might be out of date 20 | del sys.path[-1] 21 | return module 22 | 23 | 24 | def read(*rnames): 25 | return open(os.path.join(os.path.dirname(__file__), *rnames)).read() 26 | 27 | 28 | constants = import_path( 29 | os.path.join( 30 | os.path.dirname(__file__), 'src', 'tarman', 'constants' 31 | ) 32 | ) 33 | 34 | long_description = \ 35 | read('README.rst').format(help_string=constants.HELP_STRING) + \ 36 | read('docs', 'HISTORY.rst') + \ 37 | read('docs', 'LICENSE') 38 | 39 | setup( 40 | name='tarman', 41 | version="0.1.3", 42 | description="", 43 | long_description=long_description, 44 | classifiers=[ 45 | "Programming Language :: Python", 46 | "Environment :: Console :: Curses", 47 | "Topic :: System :: Archiving", 48 | ], 49 | keywords='tar zip archive curses', 50 | author='Matej Cotman', 51 | author_email='cotman.matej@gmail.com', 52 | url='https://github.com/matejc/tarman', 53 | license='BSD', 54 | packages=find_packages('src'), 55 | package_dir={'': 'src'}, 56 | zip_safe=False, 57 | install_requires=[ 58 | 'setuptools', 59 | ], 60 | tests_require=[ 61 | 'mock', 62 | 'nose', 63 | 'unittest2' 64 | ], 65 | entry_points={ 66 | 'console_scripts': [ 67 | "tarman = tarman:main", 68 | ] 69 | }, 70 | test_suite='nose.collector', 71 | package_data={ 72 | 'tarman': 73 | [ 74 | 'tests/testdata/testdata/a/ac', 75 | 'tests/testdata/testdata/a/ab/.abb', 76 | 'tests/testdata/testdata/a/aa/aaa', 77 | 'tests/testdata/testdata/c', 78 | 'tests/testdata/testdata/b/ba/baa/baab', 79 | 'tests/testdata/testdata/b/ba/baa/baaa/baaaa', 80 | 'tests/testdata/testdata.tar.gz', 81 | 'tests/testdata/tešt.tar', 82 | 'tests/testdata/corrupted.tar', 83 | ] 84 | }, 85 | ) 86 | -------------------------------------------------------------------------------- /src/tarman/__init__.py: -------------------------------------------------------------------------------- 1 | from tarman.constants import HEADER_LNS 2 | from tarman.constants import HELP_STRING 3 | from tarman.constants import ITEMS_WARNING 4 | from tarman.containers import Archive 5 | from tarman.containers import FileSystem 6 | from tarman.exceptions import OutOfRange 7 | from tarman.helpers import get_archive_class 8 | from tarman.overlaywin import PathWin 9 | from tarman.overlaywin import QuestionWin 10 | from tarman.overlaywin import TextWin 11 | from tarman.overlaywin import WorkWin 12 | from tarman.tree import DirectoryTree 13 | from tarman.viewarea import ViewArea 14 | 15 | import curses 16 | import curses.textpad 17 | import locale 18 | import logging 19 | import os 20 | import pwd 21 | import sys 22 | import traceback 23 | 24 | 25 | class Main(object): 26 | 27 | def __init__(self, mainscr, stdscr, directory, encoding): 28 | self.encoding = encoding 29 | self.header_lns = HEADER_LNS 30 | self.mainscr = mainscr 31 | self.stdscr = stdscr 32 | self.color = curses.has_colors() 33 | if self.color: 34 | # set file type attributes (color and bold) 35 | curses.init_pair(1, curses.COLOR_BLUE, -1) 36 | self.attr_folder = curses.color_pair(1) | curses.A_BOLD 37 | curses.init_pair(2, 7, -1) 38 | self.attr_norm = curses.color_pair(2) 39 | 40 | # set wright / wrong attributes (color and bold) 41 | curses.init_pair(3, curses.COLOR_GREEN, -1) 42 | self.attr_wright = curses.color_pair(3) | curses.A_BOLD 43 | 44 | curses.init_pair(4, curses.COLOR_RED, -1) 45 | self.attr_wrong = curses.color_pair(4) | curses.A_BOLD 46 | 47 | self.kill = False 48 | self.ch = -1 49 | self.visited = {} 50 | self.area = None 51 | self.container = FileSystem() 52 | self.directory = self.container.abspath(directory) 53 | self.checked = DirectoryTree(self.directory, self.container) 54 | self.chdir(self.directory) 55 | 56 | def header(self, prefix, path): 57 | h, w = self.mainscr.getmaxyx() 58 | sep = " " 59 | length = len(prefix) + len(path) + len(sep) 60 | empty = 0 61 | if length > w: 62 | path = "..." + path[length - w + 3:] 63 | else: 64 | empty = w - length 65 | self.mainscr.addstr( 66 | 0, 0, 67 | "{0}{1}{2}{3}".format( 68 | prefix, sep, path, empty * ' ' 69 | ).encode(self.encoding) 70 | ) 71 | self.mainscr.refresh() 72 | 73 | def identify_container_and_checked(self, path): 74 | if self.container.isenterable(path): # is folder 75 | return self.container, self.checked 76 | 77 | # force one-level archive browsing 78 | if not isinstance(self.container, FileSystem): 79 | return None, None 80 | 81 | aclass = get_archive_class(path) 82 | 83 | if not aclass: 84 | return None, None 85 | 86 | workwin = WorkWin(self) 87 | workwin.show("Working ...") 88 | 89 | newcontainer = aclass(path) 90 | newchecked = DirectoryTree(path, newcontainer) 91 | 92 | workwin.close() 93 | 94 | return newcontainer, newchecked 95 | 96 | def chdir(self, newpath): 97 | if newpath is None: 98 | return False 99 | 100 | if not newpath.startswith(self.directory): 101 | return False 102 | 103 | try: 104 | if self.area is None: 105 | oldsel = 0 106 | oldpath = self.directory 107 | else: 108 | oldsel = self.area.selected 109 | oldpath = self.area.abspath 110 | 111 | oldcontainer = self.container 112 | oldchecked = self.checked 113 | 114 | if newpath in self.visited: 115 | newsel, newcontainer, newchecked = self.visited[newpath] 116 | else: 117 | newcontainer, newchecked = \ 118 | self.identify_container_and_checked(newpath) 119 | if newcontainer is None: 120 | return False 121 | newsel = 0 122 | 123 | self.visited[oldpath] = [oldsel, oldcontainer, oldchecked] 124 | logging.info("OLD - {0} - {1} - {2}".format( 125 | oldpath, oldsel, oldcontainer.__class__.__name__ 126 | )) 127 | logging.info("NEW - {0} - {1} - {2}".format( 128 | newpath, newsel, newcontainer.__class__.__name__ 129 | )) 130 | 131 | h, w = self.stdscr.getmaxyx() 132 | self.container = newcontainer 133 | self.checked = newchecked 134 | 135 | self.area = ViewArea( 136 | newpath, h, newcontainer 137 | ) 138 | 139 | self.header( 140 | "{0}".format( 141 | self.container.__class__.__name__ 142 | ), 143 | self.area.abspath 144 | ) 145 | self.area.set_params(h, offset=newsel) 146 | self.refresh_scr() 147 | 148 | return True 149 | except OutOfRange: 150 | logging.error("OutOfRange .. {0}".format(newpath)) 151 | curses.flash() 152 | 153 | def insert_line(self, y, item): 154 | i, name, abspath = item 155 | self.stdscr.addstr( 156 | y, 0, 157 | "[{0}]".format( 158 | '*' if abspath in self.checked else ' ' 159 | ).encode(self.encoding) 160 | ) 161 | if self.color: 162 | if self.container.isenterable(abspath): 163 | attr = self.attr_folder 164 | name = u"{0}/".format(name) 165 | else: 166 | attr = self.attr_norm 167 | 168 | self.stdscr.addstr(y, 5, name.encode(self.encoding), attr) 169 | else: 170 | if self.container.isenterable(abspath): 171 | name = u"{0}/".format(name) 172 | self.stdscr.addstr(y, 5, name.encode(self.encoding)) 173 | 174 | def refresh_scr(self): 175 | self.stdscr.clear() 176 | 177 | if not getattr(self, 'area', None): 178 | return 179 | 180 | if len(self.area) == 0: 181 | self.stdscr.addstr(1, 5, "Directory is empty!") 182 | return 183 | 184 | iitem = 0 185 | for item in self.area: 186 | self.insert_line(iitem, item) 187 | iitem += 1 188 | 189 | y = self.area.selected_local 190 | 191 | h, w = self.stdscr.getmaxyx() 192 | self.stdscr.chgat(y, 0, w, curses.A_REVERSE) 193 | self.stdscr.move(y, 1) 194 | 195 | def loop(self): 196 | while not self.kill: 197 | self.ch = self.stdscr.getch() 198 | h, w = self.stdscr.getmaxyx() 199 | 200 | if self.ch in [ord('q'), ]: 201 | self.kill = True 202 | 203 | elif self.ch == curses.KEY_UP: 204 | self.area.set_params(h, offset=-1) 205 | 206 | elif self.ch == curses.KEY_DOWN: 207 | self.area.set_params(h, offset=1) 208 | 209 | elif self.ch == curses.KEY_PPAGE: 210 | self.area.set_params(h, offset=-5) 211 | 212 | elif self.ch == curses.KEY_NPAGE: 213 | self.area.set_params(h, offset=5) 214 | 215 | elif self.ch == 32: 216 | index = self.area.selected 217 | if index == -1: 218 | curses.flash() 219 | continue 220 | abspath = self.area.get_abspath(index) 221 | if abspath in self.checked: 222 | del self.checked[abspath] 223 | else: 224 | countitems = self.container.count_items( 225 | abspath, 226 | stop_at=ITEMS_WARNING 227 | ) 228 | if countitems >= ITEMS_WARNING and \ 229 | self.show_items_warning() != 0: 230 | continue 231 | self.checked.add(abspath, sub=True) 232 | 233 | elif self.ch in [curses.KEY_RIGHT, 10, 13]: 234 | index = self.area.selected 235 | if index == -1: 236 | curses.flash() 237 | continue 238 | abspath = self.area.get_abspath(index) 239 | 240 | result = self.chdir(abspath) 241 | if not result: 242 | curses.flash() 243 | 244 | elif self.ch in [curses.KEY_LEFT, 245 | 127, curses.ascii.BS, curses.KEY_BACKSPACE]: 246 | if not self.chdir( 247 | self.container.dirname(self.area.abspath) 248 | ): 249 | curses.flash() 250 | 251 | elif self.ch in [ord('c'), ord('C')]: 252 | if isinstance(self.container, FileSystem): 253 | aclass = self.container.__class__ 254 | checked = self.checked 255 | container = self.container 256 | 257 | pathwin = PathWin(self) 258 | exitstatus, archivepath = pathwin.show( 259 | "Create archive with format/compression based on file" 260 | " extension (ENTER to confirm or ESC to cancel):", 261 | os.path.join(os.getcwd(), "NewArchive.tar.gz") 262 | ) 263 | pathwin.close() 264 | logging.info("window exitstatus: {0}, '{1}'".format( 265 | exitstatus, archivepath 266 | )) 267 | if exitstatus != 0: 268 | continue 269 | 270 | archivepath = os.path.abspath(archivepath) 271 | aclass = get_archive_class(archivepath) 272 | if aclass is None: 273 | curses.flash() 274 | continue 275 | 276 | created = aclass.create(container, archivepath, checked) 277 | 278 | if created: 279 | TextWin(self).show( 280 | "Successfully created archive:\n{0}".format( 281 | archivepath 282 | ) 283 | ) 284 | else: 285 | curses.flash() 286 | 287 | elif self.ch in [ord('e'), ord('E')]: 288 | if isinstance(self.container, Archive): 289 | aclass = self.container.__class__ 290 | archive = self.container.archive 291 | checked = self.checked 292 | container = self.container 293 | else: 294 | index = self.area.selected 295 | if index == -1: 296 | curses.flash() 297 | continue 298 | abspath = self.area.get_abspath(index) 299 | if not abspath: 300 | curses.flash() 301 | continue 302 | aclass = get_archive_class(abspath) 303 | if aclass is None: 304 | curses.flash() 305 | continue 306 | archive = aclass.open(abspath) 307 | checked = None 308 | container = None 309 | 310 | pathwin = PathWin(self) 311 | exitstatus, s = pathwin.show( 312 | "Extract to " 313 | "(press ENTER for confirmation or ESC to cancel):" 314 | ) 315 | pathwin.close() 316 | logging.info("window exitstatus: {0}, '{1}'".format( 317 | exitstatus, s 318 | )) 319 | if exitstatus != 0: 320 | continue 321 | 322 | workwin = WorkWin(self) 323 | workwin.show("Extracting ...") 324 | aclass.extract(container, archive, s, checked=checked) 325 | workwin.close() 326 | 327 | TextWin(self).show("Extracted to:\n{0}".format(s)) 328 | 329 | elif self.ch in [ord('?'), curses.KEY_F1]: 330 | curses.curs_set(0) 331 | textwin = TextWin(self) 332 | textwin.show(HELP_STRING) 333 | 334 | if self.ch != -1: 335 | self.refresh_scr() 336 | 337 | if self.kill: 338 | break 339 | 340 | def show_items_warning(self): 341 | questionwin = QuestionWin(self) 342 | return questionwin.show( 343 | "There are more than {0} items in this folder," 344 | "\ndo you really want to select it?".format( 345 | ITEMS_WARNING 346 | ) 347 | ) 348 | 349 | def cancel(self): 350 | self.kill = True 351 | 352 | 353 | def main(): 354 | 355 | if len(sys.argv) != 2: 356 | arg_directory = os.getcwd() 357 | else: 358 | 359 | if sys.argv[1] in ['-h', '--help']: 360 | print("Usage: {0} \n\n{1}".format( 361 | os.path.basename(sys.argv[0]), HELP_STRING 362 | )) 363 | sys.exit(0) 364 | 365 | arg_directory = os.path.abspath(sys.argv[1]) 366 | if not os.path.exists(arg_directory): 367 | not_exists_string = "Path does not exists '{0}'." \ 368 | .format(arg_directory) 369 | logging.error(not_exists_string) 370 | sys.exit(1) 371 | 372 | # we need faster esc delay for more responsive program 373 | os.environ['ESCDELAY'] = '25' 374 | 375 | log_file = os.path.join(pwd.getpwuid(os.getuid()).pw_dir, '.tarman.log') 376 | 377 | # check if 'log_file' is writable 378 | # os.access # returns False if file does not exists 379 | # os.access # on parent directory does not check for files inside 380 | # therefore this is the wright solution 381 | try: 382 | with open(log_file, "w") as tmp_file: 383 | tmp_file.write("#") 384 | 385 | logging.basicConfig( 386 | filename=log_file, 387 | filemode='w', level=logging.DEBUG 388 | ) 389 | except: 390 | logging.basicConfig(level=logging.DEBUG) 391 | 392 | locale.setlocale(locale.LC_ALL, '') # en_US.UTF-8 ? 393 | encoding = locale.getpreferredencoding() 394 | logging.info("Encoding: '{0}'".format(encoding)) 395 | 396 | app = None 397 | 398 | try: 399 | 400 | # Initialize curses 401 | mainscr = curses.initscr() 402 | h, w = mainscr.getmaxyx() 403 | stdscr = curses.newwin(h - HEADER_LNS, w, HEADER_LNS, 0) 404 | 405 | curses.start_color() 406 | curses.use_default_colors() 407 | 408 | # Turn off echoing of keys, and enter cbreak mode, 409 | # where no buffering is performed on keyboard input 410 | curses.noecho() 411 | curses.cbreak() 412 | 413 | # In keypad mode, escape sequences for special keys 414 | # (like the cursor keys) will be interpreted and 415 | # a special value like curses.key_left will be returned 416 | stdscr.keypad(True) 417 | 418 | # getch will block for 500ms 419 | # stdscr.timeout(500) 420 | 421 | # getch will not block 422 | # stdscr.nodelay(1) 423 | 424 | app = Main(mainscr, stdscr, arg_directory, encoding) 425 | app.loop() # Enter the main loop 426 | 427 | # Set everything back to normal 428 | stdscr.keypad(False) 429 | curses.echo() 430 | curses.nocbreak() 431 | curses.endwin() # Terminate curses 432 | except: 433 | # In the event of an error, restore the terminal 434 | # to a sane state. 435 | stdscr.keypad(False) 436 | curses.echo() 437 | curses.nocbreak() 438 | curses.endwin() 439 | traceback.print_exc() # Print the exception 440 | app.cancel() 441 | 442 | logging.info(app.visited) 443 | 444 | logging.info(str(app.checked)) 445 | for item in app.checked: 446 | logging.info(str(item.get_data_array())) 447 | 448 | 449 | if __name__ == "__main__": 450 | main() 451 | -------------------------------------------------------------------------------- /src/tarman/constants.py: -------------------------------------------------------------------------------- 1 | 2 | HEADER_LNS = 1 3 | ITEMS_WARNING = 10000 4 | HELP_STRING = """Browser window key bindings: 5 | - c - create archive from selected files 6 | - e - extract selected files 7 | - ?/F1 - help window 8 | - LEFT/BACKSPACE - go one directory up 9 | - q - quit 10 | - RIGHT/ENTER - go in to directory or archive 11 | - SPACE - select and unselect files 12 | - UP/DOWN - move up or down in browser 13 | 14 | Overlay window key bindings: 15 | - ENTER - confirm/ok 16 | - ESC - cancel/close""" 17 | -------------------------------------------------------------------------------- /src/tarman/containers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tarfile 3 | import zipfile 4 | 5 | from tarman.exceptions import NotImplemented 6 | from tarman.tree import DirectoryTree 7 | 8 | 9 | class Container(): 10 | 11 | def listdir(self, path): 12 | raise NotImplemented() 13 | 14 | def isenterable(self, path): 15 | raise NotImplemented() 16 | 17 | def abspath(self, path): 18 | raise NotImplemented() 19 | 20 | def dirname(self, path): 21 | return os.path.dirname(path) 22 | 23 | def basename(self, path): 24 | return os.path.basename(path) 25 | 26 | def join(self, *parts): 27 | return os.path.join(*parts) 28 | 29 | def split(self, path): 30 | if path[-1] == os.sep: 31 | path = path[:-1] 32 | return os.path.split(path) 33 | 34 | def samefile(self, f1, f2): 35 | return f1.lower() == f2.lower() 36 | 37 | def count_items(self, path, stop_at=-1): 38 | count = 0 39 | if self.isenterable(path): 40 | for name in self.listdir(path): 41 | count += self.count_items(self.join(path, name), stop_at) 42 | else: 43 | count += 1 44 | return count 45 | 46 | 47 | class Archive(): 48 | 49 | def __init__(self, path): 50 | raise NotImplemented() 51 | 52 | @staticmethod 53 | def isarchive(path): 54 | raise NotImplemented() 55 | 56 | @staticmethod 57 | def open(path): 58 | raise NotImplemented() 59 | 60 | @staticmethod 61 | def extract(container, archive, target_path, checked=None): 62 | raise NotImplemented() 63 | 64 | 65 | class FileSystem(Container): 66 | 67 | def listdir(self, path): 68 | try: 69 | return os.listdir(path) 70 | except OSError: 71 | return [] 72 | 73 | def isenterable(self, path): 74 | return os.path.isdir(path) 75 | 76 | def abspath(self, path): 77 | return os.path.abspath(path) 78 | 79 | def dirname(self, path): 80 | return os.path.dirname(path) 81 | 82 | def basename(self, path): 83 | return os.path.basename(path) 84 | 85 | def join(self, *parts): 86 | return os.path.join(*parts) 87 | 88 | def split(self, path): 89 | return os.path.split(path) 90 | 91 | def samefile(self, f1, f2): 92 | return os.path.samefile(f1, f2) 93 | 94 | def count_items(self, path, stop_at=-1): 95 | n = 0 96 | 97 | for _, dirs, files in os.walk(path): 98 | for f in files: 99 | n += 1 100 | if n == stop_at: 101 | return n 102 | for d in dirs: 103 | n += 1 104 | if n == stop_at: 105 | return n 106 | return n 107 | 108 | 109 | class Tar(Container, Archive): 110 | 111 | def __init__(self, path): 112 | self.path = os.path.abspath(path) 113 | self.archive = Tar.open(self.path) 114 | self.tree = DirectoryTree(self.path, self) 115 | names = self.archive.getnames() 116 | for n in names: 117 | self.tree.add(os.path.join(self.path, n)) 118 | 119 | def listdir(self, path): 120 | children = self.tree[path].children 121 | return [c.data for c in children] 122 | 123 | def isenterable(self, path): 124 | arr = self.tree[path].get_data_array()[1:] 125 | return self.archive.getmember(os.sep.join(arr)).isdir() 126 | 127 | def abspath(self, path): 128 | return self.tree[path].get_path() 129 | 130 | @staticmethod 131 | def isarchive(path): 132 | return tarfile.is_tarfile(path) 133 | 134 | @staticmethod 135 | def open(path): 136 | return tarfile.open(path) 137 | 138 | @staticmethod 139 | def extract(container, archive, target_path, checked=None): 140 | if checked: 141 | members = [] 142 | for node in checked: 143 | # without root data 144 | arr = container.tree[node.get_path()].get_data_array()[1:] 145 | if arr[0] == '..': 146 | continue 147 | members += [archive.getmember(os.sep.join(arr))] 148 | else: 149 | members = None 150 | archive.extractall(path=target_path, members=members) 151 | 152 | 153 | class Zip(Container, Archive): 154 | 155 | def __init__(self, path): 156 | self.path = os.path.abspath(path) 157 | self.archive = Zip.open(self.path) 158 | self.tree = DirectoryTree(self.path, self) 159 | names = self.archive.namelist() 160 | for n in names: 161 | if n[-1] == os.sep: 162 | continue 163 | self.tree.add(os.path.join(self.path, n)) 164 | 165 | def listdir(self, path): 166 | children = self.tree[path].children 167 | return [c.data for c in children] 168 | 169 | def isenterable(self, path): 170 | children = self.tree[path].children 171 | return True if children else False 172 | 173 | def abspath(self, path): 174 | return self.tree[path].get_path() 175 | 176 | @staticmethod 177 | def isarchive(path): 178 | return zipfile.is_zipfile(path) 179 | 180 | @staticmethod 181 | def open(path): 182 | return zipfile.ZipFile(file=path) 183 | 184 | @staticmethod 185 | def extract(container, archive, target_path, checked=None): 186 | if checked: 187 | members = [] 188 | for node in checked: 189 | # without root data 190 | arr = container.tree[node.get_path()].get_data_array()[1:] 191 | if arr[0] == '..': 192 | continue 193 | members += [archive.getinfo(os.sep.join(arr))] 194 | else: 195 | members = None 196 | archive.extractall(path=target_path, members=members) 197 | 198 | -------------------------------------------------------------------------------- /src/tarman/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class TarmanError(Exception): 4 | pass 5 | 6 | 7 | class NotImplemented(TarmanError): 8 | pass 9 | 10 | 11 | class AlreadyExists(TarmanError): 12 | def __init__(self, message, child): 13 | super(AlreadyExists, self).__init__(message) 14 | self.child = child 15 | 16 | 17 | class FileError(TarmanError, IOError): 18 | pass 19 | 20 | 21 | class NotFound(FileError): 22 | pass 23 | 24 | 25 | class OutOfRange(FileError): 26 | pass 27 | -------------------------------------------------------------------------------- /src/tarman/helpers.py: -------------------------------------------------------------------------------- 1 | 2 | import tarman.containers 3 | 4 | import codecs 5 | import inspect 6 | import sys 7 | import os 8 | 9 | 10 | def get_archive_class(path): 11 | classes = inspect.getmembers( 12 | sys.modules[tarman.containers.__name__], inspect.isclass 13 | ) 14 | 15 | for c in classes: 16 | if c[0] == 'Archive': 17 | continue 18 | methods = inspect.getmembers(c[1], inspect.isfunction) 19 | for m in methods: 20 | if m[0] == 'isarchive': 21 | if m[1](path): 22 | return c[1] 23 | return None 24 | 25 | 26 | def container(path): 27 | aclass = get_archive_class(path) 28 | return aclass(path) if aclass else None 29 | 30 | 31 | def makepath(path): 32 | try: 33 | os.makedirs(path) 34 | return True 35 | except: 36 | return False 37 | 38 | -------------------------------------------------------------------------------- /src/tarman/overlaywin.py: -------------------------------------------------------------------------------- 1 | 2 | from tarman.exceptions import NotImplemented 3 | 4 | import curses 5 | import os 6 | import threading 7 | import time 8 | 9 | 10 | class OverlayWin(): 11 | 12 | def __init__(self, main): 13 | self.main = main 14 | 15 | def show(self, *args): 16 | raise NotImplemented() 17 | 18 | def close(self): 19 | raise NotImplemented() 20 | 21 | 22 | class TextWin(OverlayWin): 23 | 24 | def show(self, *args): 25 | text = args[0] 26 | 27 | lines, columns = self.main.stdscr.getmaxyx() 28 | 29 | textdata = text.split('\n') 30 | height = len(textdata) + 4 31 | width = max([len(s) for s in textdata]) + 4 32 | 33 | self.showing = True 34 | self.exitstatus = -1 35 | self.newwin = self.main.stdscr.derwin( 36 | height, width, 37 | (lines // 2) - (height // 2), (columns // 2) - (width // 2) 38 | ) 39 | self.newwin.clear() 40 | self.newwin.border() 41 | self.newwin.touchwin() 42 | self.newwin.refresh() 43 | 44 | for i in range(len(textdata)): 45 | self.newwin.addstr(i + 2, 2, textdata[i]) 46 | 47 | while self.showing: 48 | self.ch = self.newwin.getch() 49 | if self.ch: 50 | self.close() 51 | 52 | self.main.mainscr.touchwin() 53 | self.main.mainscr.refresh() 54 | self.main.stdscr.touchwin() 55 | self.main.stdscr.refresh() 56 | self.main.refresh_scr() 57 | 58 | def close(self): 59 | self.showing = False 60 | 61 | 62 | class QuestionWin(OverlayWin): 63 | 64 | def show(self, *args): 65 | text = args[0] 66 | 67 | self.exitstatus = -1 68 | lines, columns = self.main.stdscr.getmaxyx() 69 | 70 | text += "\n\nPress ESC to Cancel or ENTER for Ok!" 71 | 72 | textdata = text.split('\n') 73 | height = len(textdata) + 4 74 | width = max([len(s) for s in textdata]) + 4 75 | 76 | self.showing = True 77 | self.exitstatus = -1 78 | self.newwin = self.main.stdscr.derwin( 79 | height, width, 80 | (lines // 2) - (height // 2), (columns // 2) - (width // 2) 81 | ) 82 | self.newwin.clear() 83 | self.newwin.border() 84 | self.newwin.touchwin() 85 | self.newwin.refresh() 86 | 87 | for i in range(len(textdata)): 88 | self.newwin.addstr(i + 2, 2, textdata[i]) 89 | 90 | while self.showing: 91 | self.ch = self.newwin.getch() 92 | if self.ch in [27]: 93 | self.close() 94 | self.exitstatus = 1 95 | elif self.ch in [10, 13]: 96 | self.close() 97 | self.exitstatus = 0 98 | 99 | self.main.mainscr.touchwin() 100 | self.main.mainscr.refresh() 101 | self.main.stdscr.touchwin() 102 | self.main.stdscr.refresh() 103 | self.main.refresh_scr() 104 | 105 | return self.exitstatus 106 | 107 | def close(self): 108 | self.showing = False 109 | 110 | 111 | class WorkWin(OverlayWin): 112 | 113 | def show(self, *args): 114 | text = args[0] 115 | 116 | lines, columns = self.main.stdscr.getmaxyx() 117 | self.showing = True 118 | self.exitstatus = -1 119 | w = columns // 2 120 | self.newwin = self.main.stdscr.derwin( 121 | 3, w, (lines // 2) - (3 // 2), w - (columns // 4) 122 | ) 123 | self.newwin.clear() 124 | self.newwin.nodelay(1) 125 | curses.curs_set(0) 126 | self.text = text 127 | self.newwin.border() 128 | self.newwin.addstr(1, 1, text) 129 | self.newwin.touchwin() 130 | self.newwin.refresh() 131 | 132 | def run(handle, w): 133 | i = 1 134 | while handle.showing: 135 | handle.newwin.chgat(1, i, 1, curses.A_REVERSE) 136 | handle.newwin.refresh() 137 | time.sleep(0.1) 138 | handle.newwin.chgat(1, i, 1, curses.A_NORMAL) 139 | handle.newwin.refresh() 140 | i += 1 141 | if i == w - 1: 142 | i = 1 143 | 144 | self.newwin.nodelay(0) 145 | curses.curs_set(1) 146 | handle.main.mainscr.touchwin() 147 | handle.main.mainscr.refresh() 148 | handle.main.stdscr.touchwin() 149 | handle.main.stdscr.refresh() 150 | handle.main.refresh_scr() 151 | 152 | t = threading.Thread(target=run, args=(self, w)) 153 | t.setDaemon(True) 154 | t.start() 155 | 156 | def close(self): 157 | self.showing = False 158 | 159 | 160 | class PathWin(OverlayWin): 161 | 162 | def show(self, *args): 163 | """ 164 | exitstatus: 165 | -2: path not exists 166 | -1: internal error 167 | 0: all ok 168 | 1: canceled 169 | """ 170 | text = args[0] 171 | if len(args) == 2: 172 | inputtext = args[1] 173 | else: 174 | inputtext = None 175 | lines, columns = self.main.stdscr.getmaxyx() 176 | self.showing = True 177 | self.exitstatus = -1 178 | self.newwin = self.main.stdscr.derwin( 179 | 5, columns, (lines // 2) - (5 // 2), 0 180 | ) 181 | self.text = text 182 | self.newwin.clear() 183 | self.newwin.border() 184 | self.newwin.addstr(1, 1, text) 185 | self.newwin.touchwin() 186 | self.newwin.refresh() 187 | 188 | self.textwin = self.newwin.derwin( 189 | 2, columns - 2, 2, 1 190 | ) 191 | self.textwin.touchwin() 192 | self.textwin.refresh() 193 | 194 | self.textbox = curses.textpad.Textbox(self.textwin, insert_mode=True) 195 | self.textbox.stripspaces = 1 196 | 197 | def run(handle): 198 | while handle.showing and getattr(handle, 'textwin', None): 199 | handle.refresh_exists() 200 | time.sleep(0.1) 201 | 202 | t = threading.Thread(target=run, args=(self, )) 203 | t.setDaemon(True) 204 | t.start() 205 | 206 | self.textwin.addstr(0, 0, inputtext if inputtext else "") 207 | 208 | s = self.textbox.edit(self.text_validator) 209 | self.showing = False 210 | 211 | self.main.mainscr.touchwin() 212 | self.main.mainscr.refresh() 213 | self.main.stdscr.touchwin() 214 | self.main.stdscr.refresh() 215 | self.main.refresh_scr() 216 | 217 | s = s.replace('\n', '').strip() 218 | 219 | return self.exitstatus, s 220 | 221 | def text_validator(self, ch): 222 | y, x = self.textwin.getyx() 223 | maxy, maxx = self.textwin.getmaxyx() 224 | 225 | if ch in [27]: 226 | self.exitstatus = 1 227 | self.close() 228 | 229 | elif ch in [127, curses.ascii.BS, curses.KEY_BACKSPACE]: 230 | if x > 0: 231 | self.textwin.move(y, x - 1) 232 | self.textwin.delch() 233 | elif y == 0: 234 | pass 235 | else: 236 | self.textwin.move(y - 1, maxx - 1) 237 | self.textwin.delch() 238 | 239 | elif ch in [330]: 240 | self.textwin.delch() 241 | 242 | elif ch in [10, 13]: 243 | self.exitstatus = 0 244 | self.close() 245 | 246 | elif ch in [curses.KEY_HOME]: 247 | self.textwin.move(0, 0) 248 | 249 | elif ch in [curses.KEY_END]: 250 | self.textwin.move(maxy - 1, maxx - 1) 251 | 252 | return ch 253 | 254 | def parse_gather_path(self): 255 | s = '' 256 | maxy, maxx = self.textwin.getmaxyx() 257 | cy, cx = self.textwin.getyx() 258 | 259 | for y in range(maxy): 260 | self.textwin.move(y, 0) 261 | for x in range(maxx): 262 | s += chr(curses.ascii.ascii(self.textwin.inch(y, x))) 263 | 264 | self.textwin.move(cy, cx) 265 | return s.replace('\n', '').strip() 266 | 267 | def refresh_exists(self): 268 | path = self.parse_gather_path() 269 | 270 | exists = os.path.exists(path) 271 | 272 | s = " Current path exist " if exists else \ 273 | "Current path doesn't exist" 274 | 275 | self.newwin.touchwin() 276 | self.newwin.refresh() 277 | 278 | if self.main.color: 279 | self.newwin.addstr( 280 | 1, 281 | # self.newwin.getmaxyx()[1] / 2 - len(s) / 2, 282 | len(self.text) + 5, 283 | s, 284 | self.main.attr_wright if exists else self.main.attr_wrong 285 | ) 286 | else: 287 | self.newwin.addstr( 288 | 1, 289 | # self.newwin.getmaxyx()[1] / 2 - len(s) / 2, 290 | len(self.text) + 5, 291 | s, 292 | ) 293 | 294 | self.textwin.touchwin() 295 | self.textwin.refresh() 296 | 297 | def close(self): 298 | if self.textbox: # force window to close instantaneously 299 | self.textbox.do_command = lambda x: False 300 | self.showing = False 301 | -------------------------------------------------------------------------------- /src/tarman/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matejc/tarman/bc997b3653ac15bc94ef1cc95746eb36108c028f/src/tarman/tests/__init__.py -------------------------------------------------------------------------------- /src/tarman/tests/test_containers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | from tarman.containers import Container 4 | from tarman.containers import FileSystem 5 | 6 | import os 7 | import tarman.tests.test_containers 8 | import tarman.tests.test_tree 9 | import unittest2 as unittest 10 | 11 | 12 | class TestFileSystem(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.fs = FileSystem() 16 | self.testcwd = os.getcwd() 17 | self.testfilepath = tarman.tests.test_containers.__file__ 18 | self.testdirectory = os.path.dirname(self.testfilepath) 19 | self.testdatadir = os.path.join(self.testdirectory, 'testdata') 20 | 21 | def test_container(self): 22 | self.assertTrue(isinstance(self.fs, Container)) 23 | 24 | def test_listdir(self): 25 | self.assertEqual( 26 | self.fs.listdir(self.testdirectory), 27 | os.listdir(self.testdirectory) 28 | ) 29 | 30 | def test_isenterable(self): 31 | self.assertTrue(self.fs.isenterable(self.testdirectory)) 32 | 33 | def test_abspath(self): 34 | self.assertEqual(self.fs.abspath('.'), self.testcwd) 35 | 36 | def test_dirname(self): 37 | self.assertEqual( 38 | self.fs.dirname(self.testfilepath), self.testdirectory 39 | ) 40 | 41 | def test_basename(self): 42 | self.assertEqual( 43 | self.fs.basename(self.testfilepath), 44 | os.path.basename(self.testfilepath) 45 | ) 46 | 47 | def test_join(self): 48 | self.assertEqual( 49 | self.fs.join('/home', 'someone', 'bin', 'python'), 50 | '/home/someone/bin/python' 51 | ) 52 | 53 | def test_split(self): 54 | self.assertEqual( 55 | self.fs.split('/home/someone/bin/python'), 56 | ('/home/someone/bin', 'python') 57 | ) 58 | 59 | def test_samefile(self): 60 | self.assertTrue( 61 | self.fs.samefile(self.testfilepath, self.testfilepath) 62 | ) 63 | self.assertFalse( 64 | self.fs.samefile( 65 | self.testfilepath, 66 | tarman.tests.test_tree.__file__ 67 | ) 68 | ) 69 | 70 | def test_count_items(self): 71 | self.assertEqual( 72 | self.fs.count_items(self.testdatadir), 73 | 17 74 | ) 75 | self.assertEqual( 76 | self.fs.count_items(self.testdatadir, stop_at=9), 77 | 9 78 | ) 79 | -------------------------------------------------------------------------------- /src/tarman/tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tarman.tests.test_containers 3 | import unittest2 as unittest 4 | 5 | 6 | class TestHelpers(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.testfilepath = tarman.tests.test_containers.__file__ 10 | self.testdirectory = os.path.dirname(self.testfilepath) 11 | self.testarchivepath = os.path.join( 12 | self.testdirectory, 'testdata', 'testdata.tar.gz' 13 | ) 14 | 15 | def utf8_return_function(self): 16 | return "sheeps" 17 | 18 | def test_utf8_return(self): 19 | self.assertIsInstance(self.utf8_return_function(), str) 20 | 21 | def utf8_args_by_index_function(self, text1=None, text2=None): 22 | return text1, text2 23 | 24 | def test_utf8_args_by_index(self): 25 | text1, text2 = self.utf8_args_by_index_function("sheeps1", "sheeps2") 26 | self.assertIsInstance(text1, str) 27 | self.assertIsInstance(text2, str) 28 | 29 | def utf8_args_by_keyword_function(self, text1=None, text2=None): 30 | return text1, text2 31 | 32 | def test_utf8_args_by_keyword(self): 33 | text1, text2 = self.utf8_args_by_keyword_function( 34 | "sheeps1", text2="sheeps2" 35 | ) 36 | self.assertIsInstance(text1, str) 37 | self.assertIsInstance(text2, str) 38 | 39 | def utf8_args_combo_function(self, text1=None, text2=None): 40 | return text1, text2 41 | 42 | def test_utf8_args_combo(self): 43 | text1, text2 = self.utf8_args_by_keyword_function( 44 | "sheeps1", text2="sheeps2" 45 | ) 46 | self.assertIsInstance(text1, str) 47 | self.assertIsInstance(text2, str) 48 | -------------------------------------------------------------------------------- /src/tarman/tests/test_tree.py: -------------------------------------------------------------------------------- 1 | 2 | from tarman.containers import FileSystem 3 | from tarman.exceptions import OutOfRange 4 | from tarman.tree import DirectoryTree 5 | 6 | import os 7 | import tarman 8 | import tempfile 9 | import unittest2 as unittest 10 | 11 | 12 | class TestDirectoryTree(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.testfilepath = tarman.tests.test_containers.__file__ 16 | self.testdirectory = os.path.dirname(self.testfilepath) 17 | self.testdatapath = os.path.join( 18 | self.testdirectory, 'testdata', 'testdata' 19 | ) 20 | self.fs = FileSystem() 21 | 22 | def test_init(self): 23 | self.assertIsNotNone( 24 | DirectoryTree(self.testdatapath, self.fs) 25 | ) 26 | 27 | def test_add_file(self): 28 | tree = DirectoryTree(self.testdatapath, self.fs) 29 | path1 = self.fs.join(self.testdatapath, 'a', 'aa', 'aaa') 30 | path2 = self.fs.join(self.testdatapath, 'a', 'aa') 31 | tree.add(path1) 32 | self.assertIn(path1, tree) 33 | self.assertIn(path2, tree) # not-added one-level-up directory 34 | 35 | def test_add_dir(self): 36 | tree = DirectoryTree(self.testdatapath, self.fs) 37 | dir1 = self.fs.join(self.testdatapath, 'a', 'ab') 38 | tree.add(dir1) 39 | self.assertIn(dir1, tree) 40 | 41 | def test_out_of_range(self): 42 | tree = DirectoryTree(self.testdatapath, self.fs) 43 | 44 | # one level up directory 45 | dir1 = self.fs.abspath(self.fs.join(self.testdatapath, '..')) 46 | with self.assertRaises(OutOfRange): 47 | tree.add(dir1) 48 | 49 | # completely different directory 50 | with tempfile.NamedTemporaryFile() as f: 51 | with self.assertRaises(OutOfRange): 52 | tree.add(f.name) 53 | -------------------------------------------------------------------------------- /src/tarman/tests/testdata/corrupted.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matejc/tarman/bc997b3653ac15bc94ef1cc95746eb36108c028f/src/tarman/tests/testdata/corrupted.tar -------------------------------------------------------------------------------- /src/tarman/tests/testdata/testdata.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matejc/tarman/bc997b3653ac15bc94ef1cc95746eb36108c028f/src/tarman/tests/testdata/testdata.tar.gz -------------------------------------------------------------------------------- /src/tarman/tests/testdata/testdata/a/aa/aaa: -------------------------------------------------------------------------------- 1 | aaa 2 | -------------------------------------------------------------------------------- /src/tarman/tests/testdata/testdata/a/ab/.abb: -------------------------------------------------------------------------------- 1 | hidden 2 | -------------------------------------------------------------------------------- /src/tarman/tests/testdata/testdata/a/ac: -------------------------------------------------------------------------------- 1 | ac 2 | -------------------------------------------------------------------------------- /src/tarman/tests/testdata/testdata/b/ba/baa/baaa/baaaa: -------------------------------------------------------------------------------- 1 | baaaa -------------------------------------------------------------------------------- /src/tarman/tests/testdata/testdata/b/ba/baa/baab: -------------------------------------------------------------------------------- 1 | baab -------------------------------------------------------------------------------- /src/tarman/tests/testdata/testdata/c: -------------------------------------------------------------------------------- 1 | c -------------------------------------------------------------------------------- /src/tarman/tests/testdata/tešt.tar: -------------------------------------------------------------------------------- 1 | tarmanš.log0000644000175000017510000000061212157671221012577 0ustar matejmatejINFO:root:OLD - /home/matej/Dropbox/matej/workarea/pys/tarman_2 - 0 - FileSystem 2 | INFO:root:NEW - /home/matej/Dropbox/matej/workarea/pys/tarman_2 - 0 - FileSystem 3 | INFO:root:{'/home/matej/Dropbox/matej/workarea/pys/tarman_2': [0, , ]} 4 | INFO:root: 5 | -------------------------------------------------------------------------------- /src/tarman/tree.py: -------------------------------------------------------------------------------- 1 | from tarman.exceptions import NotFound 2 | from tarman.exceptions import AlreadyExists 3 | from tarman.exceptions import OutOfRange 4 | 5 | 6 | class Node(): 7 | 8 | def __init__(self, data, parent=None, children=[]): 9 | self.parent = parent 10 | self.children = children 11 | self.data = data 12 | 13 | def add_child(self, data): 14 | tmp = Node(data=data, parent=self) 15 | if self.children: 16 | self.children += [tmp] 17 | else: 18 | self.children = [tmp] 19 | return tmp 20 | 21 | def __iter__(self): 22 | for child in self.children: 23 | if child.children: 24 | for c in child: 25 | yield c 26 | else: 27 | yield child 28 | 29 | def get_array(self): 30 | result = [] 31 | tmp = self 32 | while tmp is not None: 33 | result = [tmp] + result 34 | tmp = tmp.parent 35 | return result 36 | 37 | def get_data_array(self): 38 | result = [] 39 | tmp = self 40 | while tmp is not None: 41 | result = [tmp.data] + result 42 | tmp = tmp.parent 43 | return result 44 | 45 | def __str__(self): 46 | return self.data 47 | 48 | def get_child(self, data): 49 | for c in self.children: 50 | if c.data == data: 51 | return c 52 | return None 53 | 54 | def del_self(self): 55 | if self.parent is None: 56 | return 57 | for i in range(len(self.parent.children)): 58 | if self.parent.children[i].data == self.data: 59 | del self.parent.children[i] 60 | return 61 | 62 | 63 | class Tree(): 64 | 65 | def __init__(self, root_data): 66 | self.root = Node(data=root_data, parent=None) 67 | 68 | 69 | class FileNode(Node): 70 | 71 | def __init__(self, path, container, parent=None, sub=True): 72 | try: 73 | self.container = container 74 | self.parent = parent 75 | if self.parent is None: 76 | self.data = path 77 | else: 78 | self.data = self.container.basename(path) 79 | for c in self.parent.children: 80 | if self.data == c.data: 81 | raise AlreadyExists( 82 | "'{0}' is in '{1}'".format(self.data, path), 83 | c 84 | ) 85 | self.children = [] 86 | if sub and self.is_dir(): 87 | for n in self.container.listdir(path): 88 | self.add_subdir(self.container.join(path, n)) 89 | except OSError: 90 | raise NotFound(path) 91 | 92 | def is_dir(self): 93 | return self.container.isenterable(self.get_path()) 94 | 95 | def get_path(self): 96 | if self.parent: 97 | return self.container.join(*self.get_data_array()) 98 | else: 99 | return self.data 100 | 101 | def add_subdir(self, path, parent=None, sub=True): 102 | parent = parent if parent else self 103 | try: 104 | tmp = FileNode(path, self.container, parent=self, sub=sub) 105 | if self.children: 106 | self.children += [tmp] 107 | else: 108 | self.children = [tmp] 109 | return tmp 110 | except AlreadyExists as e: 111 | # self.add_subdir(path=path, parent=e.child, sub=sub) 112 | return e.child 113 | 114 | def _get_array_by_path(self, path): 115 | result = [] 116 | prefix, name = self.container.split(path) 117 | while name: 118 | result = [name] + result 119 | prefix, name = self.container.split(prefix) 120 | return result 121 | 122 | def get_children_data(self): 123 | return [c.data for c in self.children] 124 | 125 | def __eq__(self, node): 126 | if node == self: 127 | return True 128 | 129 | if not isinstance(node, FileNode): 130 | return False 131 | 132 | f1 = self.get_path() 133 | f2 = node.get_path() 134 | 135 | return self.container.samefile(f1, f2) 136 | 137 | 138 | class DirectoryTree(Tree): 139 | 140 | def __init__(self, root_dir, container): 141 | self.root_dir = root_dir 142 | self.container = container 143 | self.root = FileNode(self.root_dir, self.container, sub=False) 144 | 145 | def __iter__(self): 146 | return self.root.__iter__() 147 | 148 | def add(self, path, sub=False): 149 | main_array = self.root._get_array_by_path(self.root_dir) 150 | path_array = self.root._get_array_by_path(path) 151 | 152 | d = self.root 153 | len_main = len(main_array) 154 | len_path = len(path_array) 155 | 156 | if len_main > len_path: 157 | raise OutOfRange(path) 158 | 159 | for i in range(len_main): 160 | if main_array[i] != path_array[i]: 161 | raise OutOfRange(path) 162 | 163 | for i in range(len_main, len_path - 1): 164 | d = d.add_subdir( 165 | self.container.join(d.get_path(), path_array[i]), 166 | sub=False 167 | ) 168 | 169 | if len_main < len_path: 170 | d = d.add_subdir( 171 | self.container.join(d.get_path(), path_array[i + 1]), 172 | sub=sub 173 | ) 174 | 175 | return d 176 | 177 | def __contains__(self, path): 178 | main_array = self.root._get_array_by_path(self.root_dir) 179 | path_array = self.root._get_array_by_path(path) 180 | 181 | d = self.root 182 | len_main = len(main_array) 183 | len_path = len(path_array) 184 | 185 | if len_main > len_path: 186 | raise OutOfRange(path) 187 | 188 | for i in range(len_main): 189 | if main_array[i] != path_array[i]: 190 | raise OutOfRange(path) 191 | 192 | for i in range(len_main, len_path): 193 | d = d.get_child(path_array[i]) 194 | if d is None: 195 | return False 196 | 197 | return True 198 | 199 | def __getitem__(self, path): 200 | main_array = self.root._get_array_by_path(self.root_dir) 201 | path_array = self.root._get_array_by_path(path) 202 | 203 | d = self.root 204 | len_main = len(main_array) 205 | len_path = len(path_array) 206 | 207 | if len_main > len_path: 208 | raise OutOfRange(path) 209 | 210 | for i in range(len_main): 211 | if main_array[i] != path_array[i]: 212 | raise OutOfRange(path) 213 | 214 | for i in range(len_main, len_path): 215 | d = d.get_child(path_array[i]) 216 | if d is None: 217 | return None 218 | if d.data == path_array[i]: 219 | continue 220 | 221 | return d 222 | 223 | def __delitem__(self, path): 224 | self[path].del_self() 225 | 226 | -------------------------------------------------------------------------------- /src/tarman/viewarea.py: -------------------------------------------------------------------------------- 1 | from tarman.tree import DirectoryTree 2 | 3 | 4 | class ViewArea(): 5 | """List of files and directories for area on screen. 6 | When you change directory, create new instance. 7 | """ 8 | 9 | def __init__(self, path, height, container): 10 | self.abspath = path 11 | self.container = container 12 | 13 | names = self.container.listdir(self.abspath) 14 | 15 | self.list = sorted(names) 16 | self.first = self.selected = 0 17 | self.last = -1 18 | self.set_params(height) 19 | 20 | def set_params(self, height, offset=0): 21 | self.height = height 22 | list_len = len(self.list) 23 | sel_old = self.selected 24 | sel_new = sel_old + offset 25 | 26 | if sel_new < 0: 27 | sel_new = 0 28 | elif sel_new >= list_len: 29 | sel_new = list_len - 1 30 | 31 | if list_len <= self.height: 32 | self.first = 0 33 | self.last = list_len - 1 34 | self.selected = sel_new 35 | else: 36 | first_old = self.first 37 | last_old = self.first + self.height - 1 38 | 39 | if sel_new > last_old: 40 | self.last = sel_new 41 | self.first += self.last - last_old 42 | elif sel_new < first_old: 43 | self.first = sel_new 44 | self.last -= first_old - self.first 45 | else: 46 | self.first = first_old 47 | self.last = last_old 48 | 49 | self.selected = sel_new 50 | 51 | self.selected_local = self.selected - self.first 52 | 53 | def get_abspath(self, index): 54 | try: 55 | return self.container.join(self.abspath, self[index]) 56 | except IndexError: 57 | return None 58 | 59 | def get_selected_abs(self): 60 | return self.get_abspath(self.selected) 61 | 62 | def get_selected_name(self): 63 | return self[self.selected] 64 | 65 | def __getitem__(self, key): 66 | return self.list[key] 67 | 68 | def __iter__(self): 69 | for i in range(self.first, self.last + 1): 70 | name = self.list[i] 71 | abspath = self.container.join(self.abspath, name) 72 | yield ( 73 | i, 74 | name, 75 | abspath 76 | ) 77 | 78 | def __len__(self): 79 | return self.last - self.first + 1 80 | --------------------------------------------------------------------------------