diff --git a/cbreader/settings/base.py b/cbreader/settings/base.py index a4e2bb7..245e3c1 100644 --- a/cbreader/settings/base.py +++ b/cbreader/settings/base.py @@ -117,7 +117,7 @@ CBREADER_USE_RECAPTCHA = os.getenv("DJANGO_CBREADER_USE_RECAPTCHA", False) RECAPTCHA_PRIVATE_KEY = os.getenv("DJANGO_RECAPTCHA_PRIVATE_KEY", '') RECAPTCHA_PUBLIC_KEY = os.getenv("DJANGO_RECAPTCHA_PUBLIC_KEY", '') -COMIC_DIR = "/media/comics" +COMIC_BOOK_VOLUME = Path(os.getenv("COMIC_BOOK_VOLUME")) from .logger import LOGGING diff --git a/comic/forms.py b/comic/forms.py index c668f45..a1d55b3 100644 --- a/comic/forms.py +++ b/comic/forms.py @@ -13,13 +13,6 @@ class InitialSetupForm(forms.Form): password_confirm = forms.CharField( widget=forms.PasswordInput(attrs={"class": "form-control"}) ) - base_dir = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) - - def clean_base_dir(self): - data = self.cleaned_data["base_dir"] - if not path.isdir(data): - raise forms.ValidationError("This is not a valid Directory") - return data def clean(self): form_data = self.cleaned_data diff --git a/comic/models.py b/comic/models.py index 55861b6..4045b8f 100644 --- a/comic/models.py +++ b/comic/models.py @@ -1,6 +1,10 @@ import uuid import zipfile -from os import listdir, path +from dataclasses import dataclass +from functools import reduce +from os import listdir +from pathlib import Path +from typing import Optional, List from django.conf import settings from django.contrib.auth.models import User @@ -11,7 +15,7 @@ import PyPDF4 import PyPDF4.utils -from comic import rarfile +import rarfile if settings.UNRAR_TOOL: rarfile.UNRAR_TOOL = settings.UNRAR_TOOL @@ -40,15 +44,18 @@ class Directory(models.Model): return "Directory: {0}; {1}".format(self.name, self.parent) @property - def path(self): + def path(self) -> Path: return self.get_path() - def get_path(self): + def get_path(self) -> Path: path_items = self.get_path_items() path_items.reverse() - return path.sep.join(path_items) + if len(path_items) >= 2: + return reduce(lambda x, y: Path(x, y), path_items) + else: + return Path(path_items[0]) - def get_path_items(self, p=None): + def get_path_items(self, p: Optional[List] = None) -> List[str]: if p is None: p = [] p.append(self.name) @@ -64,14 +71,14 @@ class Directory(models.Model): self.parent.get_path_objects(p) return p - @staticmethod - def get_dir_from_path(file_path): - file_path = file_path.split(path.sep) - print(file_path) - for d in Directory.objects.filter(name=file_path[-1]): - print(d) - if d.get_path_items() == file_path: - return d + # @staticmethod + # def get_dir_from_path(file_path): + # file_path = file_path.split(os_path.sep) + # print(file_path) + # for d in Directory.objects.filter(name=file_path[-1]): + # print(d) + # if d.get_path_items() == file_path: + # return d class ComicBook(models.Model): @@ -89,15 +96,15 @@ class ComicBook(models.Model): return urlsafe_base64_encode(self.selector.bytes) def get_pdf(self): - base_dir = Setting.objects.get(name="BASE_DIR").value - return path.join(base_dir, self.directory.get_path(), self.file_name) + base_dir = settings.COMIC_BOOK_VOLUME + return Path(base_dir, self.directory.get_path(), self.file_name) def get_image(self, page): - base_dir = Setting.objects.get(name="BASE_DIR").value + base_dir = settings.COMIC_BOOK_VOLUME if self.directory: - archive_path = path.join(base_dir, self.directory.path, self.file_name) + archive_path = Path(base_dir, self.directory.path, self.file_name) else: - archive_path = path.join(base_dir, self.file_name) + archive_path = Path(base_dir, self.file_name) try: archive = rarfile.RarFile(archive_path) except rarfile.NotRarFile: @@ -117,39 +124,23 @@ class ComicBook(models.Model): def page_count(self): return ComicPage.objects.filter(Comic=self).count() + @dataclass class Navigation: - next_index = 0 - next_path = "" - prev_index = 0 - prev_path = "" - cur_index = 0 - cur_path = "" - q_prev_to_directory = False - q_next_to_directory = False + next_path: str + prev_path: str + cur_path: str - def __init__(self, **kwargs): - for arg in kwargs: - setattr(self, arg, kwargs[arg]) + def nav(self, user): + return self.Navigation( + next_path=self.nav_get_next_comic(user), + prev_path=self.nav_get_prev_comic(user), + cur_path=urlsafe_base64_encode(self.selector.bytes) + ) - def nav(self, page, user): - out = self.Navigation(cur_index=page, cur_path=urlsafe_base64_encode(self.selector.bytes)) - if page == 0: - out.prev_path, out.prev_index = self.nav_get_prev_comic(user) - if out.prev_index == -1: - out.q_prev_to_directory = True - else: - out.prev_index = page - 1 - out.prev_path = out.cur_path - - out.next_path, out.next_index = self.nav_get_next_comic(user) - if out.next_index == -1: - out.q_next_to_directory = True - return out - - def nav_get_prev_comic(self, user): - base_dir = Setting.objects.get(name="BASE_DIR").value + def nav_get_prev_comic(self, user) -> str: + base_dir = settings.COMIC_BOOK_VOLUME if self.directory: - folder = path.join(base_dir, self.directory.path) + folder = Path(base_dir, self.directory.path) else: folder = base_dir dir_list = ComicBook.get_ordered_dir_list(folder) @@ -159,11 +150,15 @@ class ComicBook(models.Model): comic_path = urlsafe_base64_encode(self.directory.selector.bytes) else: comic_path = "" - index = -1 else: prev_comic = dir_list[comic_index - 1] - if not path.isdir(path.join(folder, prev_comic)): + if Path(folder, prev_comic).is_dir(): + if self.directory: + comic_path = urlsafe_base64_encode(self.directory.selector.bytes) + else: + comic_path = "" + else: try: if self.directory: book = ComicBook.objects.get(file_name=prev_comic, directory=self.directory) @@ -175,20 +170,14 @@ class ComicBook(models.Model): else: book = ComicBook.process_comic_book(prev_comic) cs, _ = ComicStatus.objects.get_or_create(comic=book, user=user) - index = cs.last_read_page comic_path = urlsafe_base64_encode(book.selector.bytes) - else: - if self.directory: - comic_path = urlsafe_base64_encode(self.directory.selector.bytes) - else: - comic_path = "" - index = -1 - return comic_path, index + + return comic_path def nav_get_next_comic(self, user): - base_dir = Setting.objects.get(name="BASE_DIR").value + base_dir = settings.COMIC_BOOK_VOLUME if self.directory: - folder = path.join(base_dir, self.directory.path) + folder = Path(base_dir, self.directory.path) else: folder = base_dir dir_list = ComicBook.get_ordered_dir_list(folder) @@ -216,15 +205,12 @@ class ComicBook(models.Model): if type(book) is str: raise IndexError comic_path = urlsafe_base64_encode(book.selector.bytes) - cs, _ = ComicStatus.objects.get_or_create(comic=book, user=user) - index = cs.last_read_page except IndexError: if self.directory: comic_path = urlsafe_base64_encode(self.directory.selector.bytes) else: comic_path = "" - index = -1 - return comic_path, index + return comic_path class DirFile: def __init__(self): @@ -261,11 +247,11 @@ class ComicBook(models.Model): return book except ComicBook.DoesNotExist: pass - base_dir = Setting.objects.get(name="BASE_DIR").value + base_dir = settings.COMIC_BOOK_VOLUME if directory: - comic_full_path = path.join(base_dir, directory.get_path(), comic_file_name) + comic_full_path = Path(base_dir, directory.get_path(), comic_file_name) else: - comic_full_path = path.join(base_dir, comic_file_name) + comic_full_path = Path(base_dir, comic_file_name) try: cbx = rarfile.RarFile(comic_full_path) @@ -327,7 +313,7 @@ class ComicBook(models.Model): directories = [] files = [] for item in listdir(folder): - if path.isdir(path.join(folder, item)): + if Path(folder, item).is_dir(): directories.append(item) else: files.append(item) diff --git a/comic/rarfile.py b/comic/rarfile.py deleted file mode 100644 index 451fc96..0000000 --- a/comic/rarfile.py +++ /dev/null @@ -1,2087 +0,0 @@ -# rarfile.py -# -# Copyright (c) 2005-2014 Marko Kreen -# -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -r"""RAR archive reader. - -This is Python module for Rar archive reading. The interface -is made as :mod:`zipfile`-like as possible. - -Basic logic: - - Parse archive structure with Python. - - Extract non-compressed files with Python - - Extract compressed files with unrar. - - Optionally write compressed data to temp file to speed up unrar, - otherwise it needs to scan whole archive on each execution. - -Example:: - - import rarfile - - rf = rarfile.RarFile('myarchive.rar') - for f in rf.infolist(): - print f.filename, f.file_size - if f.filename == 'README': - print(rf.read(f)) - -Archive files can also be accessed via file-like object returned -by :meth:`RarFile.open`:: - - import rarfile - - with rarfile.RarFile('archive.rar') as rf: - with rf.open('README') as f: - for ln in f: - print(ln.strip()) - -There are few module-level parameters to tune behaviour, -here they are with defaults, and reason to change it:: - - import rarfile - - # Set to full path of unrar.exe if it is not in PATH - rarfile.UNRAR_TOOL = "unrar" - - # Set to 0 if you don't look at comments and want to - # avoid wasting time for parsing them - rarfile.NEED_COMMENTS = 1 - - # Set up to 1 if you don't want to deal with decoding comments - # from unknown encoding. rarfile will try couple of common - # encodings in sequence. - rarfile.UNICODE_COMMENTS = 0 - - # Set to 1 if you prefer timestamps to be datetime objects - # instead tuples - rarfile.USE_DATETIME = 0 - - # Set to '/' to be more compatible with zipfile - rarfile.PATH_SEP = '\\' - -For more details, refer to source. - -""" - -__version__ = "2.7" - -# export only interesting items -__all__ = ["is_rarfile", "RarInfo", "RarFile", "RarExtFile"] - -## -## Imports and compat - support both Python 2.x and 3.x -## - -import errno -import os -import struct -import sys -from binascii import crc32 -from datetime import datetime -from struct import pack, unpack -from subprocess import PIPE, STDOUT, Popen -from tempfile import mkstemp - -# only needed for encryped headers -try: - from Crypto.Cipher import AES - - try: - from hashlib import sha1 - except ImportError: - from sha import new as sha1 - _have_crypto = 1 -except ImportError: - _have_crypto = 0 - -# compat with 2.x -if sys.hexversion < 0x3000000: - # prefer 3.x behaviour - range = xrange - # py2.6 has broken bytes() - def bytes(s, enc): - return str(s) - - -else: - unicode = str - -# see if compat bytearray() is needed -try: - bytearray -except NameError: - import array - - class bytearray: - def __init__(self, val=""): - self.arr = array.array("B", val) - self.append = self.arr.append - self.__getitem__ = self.arr.__getitem__ - self.__len__ = self.arr.__len__ - - def decode(self, *args): - return self.arr.tostring().decode(*args) - - -# Optimized .readinto() requires memoryview -try: - memoryview - have_memoryview = 1 -except NameError: - have_memoryview = 0 - -# Struct() for older python -try: - from struct import Struct -except ImportError: - - class Struct: - def __init__(self, fmt): - self.format = fmt - self.size = struct.calcsize(fmt) - - def unpack(self, buf): - return unpack(self.format, buf) - - def unpack_from(self, buf, ofs=0): - return unpack(self.format, buf[ofs : ofs + self.size]) - - def pack(self, *args): - return pack(self.format, *args) - - -# file object superclass -try: - from io import RawIOBase -except ImportError: - - class RawIOBase(object): - def close(self): - pass - - -## -## Module configuration. Can be tuned after importing. -## - -#: default fallback charset -DEFAULT_CHARSET = "windows-1252" - -#: list of encodings to try, with fallback to DEFAULT_CHARSET if none succeed -TRY_ENCODINGS = ("utf8", "utf-16le") - -#: 'unrar', 'rar' or full path to either one -UNRAR_TOOL = "unrar" - -#: Command line args to use for opening file for reading. -OPEN_ARGS = ("p", "-inul") - -#: Command line args to use for extracting file to disk. -EXTRACT_ARGS = ("x", "-y", "-idq") - -#: args for testrar() -TEST_ARGS = ("t", "-idq") - -# -# Allow use of tool that is not compatible with unrar. -# -# By default use 'bsdtar' which is 'tar' program that -# sits on top of libarchive. -# -# Problems with libarchive RAR backend: -# - Does not support solid archives. -# - Does not support password-protected archives. -# - -ALT_TOOL = "bsdtar" -ALT_OPEN_ARGS = ("-x", "--to-stdout", "-f") -ALT_EXTRACT_ARGS = ("-x", "-f") -ALT_TEST_ARGS = ("-t", "-f") -ALT_CHECK_ARGS = ("--help",) - -#: whether to speed up decompression by using tmp archive -USE_EXTRACT_HACK = 1 - -#: limit the filesize for tmp archive usage -HACK_SIZE_LIMIT = 20 * 1024 * 1024 - -#: whether to parse file/archive comments. -NEED_COMMENTS = 0 - -#: whether to convert comments to unicode strings -UNICODE_COMMENTS = 0 - -#: Convert RAR time tuple into datetime() object -USE_DATETIME = 0 - -#: Separator for path name components. RAR internally uses '\\'. -#: Use '/' to be similar with zipfile. -PATH_SEP = "\\" - -## -## rar constants -## - -# block types -RAR_BLOCK_MARK = 0x72 # r -RAR_BLOCK_MAIN = 0x73 # s -RAR_BLOCK_FILE = 0x74 # t -RAR_BLOCK_OLD_COMMENT = 0x75 # u -RAR_BLOCK_OLD_EXTRA = 0x76 # v -RAR_BLOCK_OLD_SUB = 0x77 # w -RAR_BLOCK_OLD_RECOVERY = 0x78 # x -RAR_BLOCK_OLD_AUTH = 0x79 # y -RAR_BLOCK_SUB = 0x7A # z -RAR_BLOCK_ENDARC = 0x7B # { - -# flags for RAR_BLOCK_MAIN -RAR_MAIN_VOLUME = 0x0001 -RAR_MAIN_COMMENT = 0x0002 -RAR_MAIN_LOCK = 0x0004 -RAR_MAIN_SOLID = 0x0008 -RAR_MAIN_NEWNUMBERING = 0x0010 -RAR_MAIN_AUTH = 0x0020 -RAR_MAIN_RECOVERY = 0x0040 -RAR_MAIN_PASSWORD = 0x0080 -RAR_MAIN_FIRSTVOLUME = 0x0100 -RAR_MAIN_ENCRYPTVER = 0x0200 - -# flags for RAR_BLOCK_FILE -RAR_FILE_SPLIT_BEFORE = 0x0001 -RAR_FILE_SPLIT_AFTER = 0x0002 -RAR_FILE_PASSWORD = 0x0004 -RAR_FILE_COMMENT = 0x0008 -RAR_FILE_SOLID = 0x0010 -RAR_FILE_DICTMASK = 0x00E0 -RAR_FILE_DICT64 = 0x0000 -RAR_FILE_DICT128 = 0x0020 -RAR_FILE_DICT256 = 0x0040 -RAR_FILE_DICT512 = 0x0060 -RAR_FILE_DICT1024 = 0x0080 -RAR_FILE_DICT2048 = 0x00A0 -RAR_FILE_DICT4096 = 0x00C0 -RAR_FILE_DIRECTORY = 0x00E0 -RAR_FILE_LARGE = 0x0100 -RAR_FILE_UNICODE = 0x0200 -RAR_FILE_SALT = 0x0400 -RAR_FILE_VERSION = 0x0800 -RAR_FILE_EXTTIME = 0x1000 -RAR_FILE_EXTFLAGS = 0x2000 - -# flags for RAR_BLOCK_ENDARC -RAR_ENDARC_NEXT_VOLUME = 0x0001 -RAR_ENDARC_DATACRC = 0x0002 -RAR_ENDARC_REVSPACE = 0x0004 -RAR_ENDARC_VOLNR = 0x0008 - -# flags common to all blocks -RAR_SKIP_IF_UNKNOWN = 0x4000 -RAR_LONG_BLOCK = 0x8000 - -# Host OS types -RAR_OS_MSDOS = 0 -RAR_OS_OS2 = 1 -RAR_OS_WIN32 = 2 -RAR_OS_UNIX = 3 -RAR_OS_MACOS = 4 -RAR_OS_BEOS = 5 - -# Compression methods - '0'..'5' -RAR_M0 = 0x30 -RAR_M1 = 0x31 -RAR_M2 = 0x32 -RAR_M3 = 0x33 -RAR_M4 = 0x34 -RAR_M5 = 0x35 - -## -## internal constants -## - -RAR_ID = bytes("Rar!\x1a\x07\x00", "ascii") -ZERO = bytes("\0", "ascii") -EMPTY = bytes("", "ascii") - -S_BLK_HDR = Struct(" HACK_SIZE_LIMIT: - use_hack = 0 - elif not USE_EXTRACT_HACK: - use_hack = 0 - - # now extract - if inf.compress_type == RAR_M0 and (inf.flags & RAR_FILE_PASSWORD) == 0: - return self._open_clear(inf) - elif use_hack: - return self._open_hack(inf, psw) - else: - return self._open_unrar(self.rarfile, inf, psw) - - def read(self, fname, psw=None): - """Return uncompressed data for archive entry. - - For longer files using :meth:`RarFile.open` may be better idea. - - Parameters: - - fname - filename or RarInfo instance - psw - password to use for extracting. - """ - - f = self.open(fname, "r", psw) - try: - return f.read() - finally: - f.close() - - def close(self): - """Release open resources.""" - pass - - def printdir(self): - """Print archive file list to stdout.""" - for f in self._info_list: - print(f.filename) - - def extract(self, member, path=None, pwd=None): - """Extract single file into current directory. - - Parameters: - - member - filename or :class:`RarInfo` instance - path - optional destination path - pwd - optional password to use - """ - if isinstance(member, RarInfo): - fname = member.filename - else: - fname = member - self._extract([fname], path, pwd) - - def extractall(self, path=None, members=None, pwd=None): - """Extract all files into current directory. - - Parameters: - - path - optional destination path - members - optional filename or :class:`RarInfo` instance list to extract - pwd - optional password to use - """ - fnlist = [] - if members is not None: - for m in members: - if isinstance(m, RarInfo): - fnlist.append(m.filename) - else: - fnlist.append(m) - self._extract(fnlist, path, pwd) - - def testrar(self): - """Let 'unrar' test the archive. - """ - cmd = [UNRAR_TOOL] + list(TEST_ARGS) - add_password_arg(cmd, self._password) - cmd.append(self.rarfile) - p = custom_popen(cmd) - output = p.communicate()[0] - check_returncode(p, output) - - def strerror(self): - """Return error string if parsing failed, - or None if no problems. - """ - return self._parse_error - - ## - ## private methods - ## - - def _set_error(self, msg, *args): - if args: - msg = msg % args - self._parse_error = msg - if self._strict: - raise BadRarFile(msg) - - # store entry - def _process_entry(self, item): - if item.type == RAR_BLOCK_FILE: - # use only first part - if (item.flags & RAR_FILE_SPLIT_BEFORE) == 0: - self._info_map[item.filename] = item - self._info_list.append(item) - # remember if any items require password - if item.needs_password(): - self._needs_password = True - elif len(self._info_list) > 0: - # final crc is in last block - old = self._info_list[-1] - old.CRC = item.CRC - old.compress_size += item.compress_size - - # parse new-style comment - if item.type == RAR_BLOCK_SUB and item.filename == "CMT": - if not NEED_COMMENTS: - pass - elif item.flags & (RAR_FILE_SPLIT_BEFORE | RAR_FILE_SPLIT_AFTER): - pass - elif item.flags & RAR_FILE_SOLID: - # file comment - cmt = self._read_comment_v3(item, self._password) - if len(self._info_list) > 0: - old = self._info_list[-1] - old.comment = cmt - else: - # archive comment - cmt = self._read_comment_v3(item, self._password) - self.comment = cmt - - if self._info_callback: - self._info_callback(item) - - # read rar - def _parse(self): - self._fd = None - try: - self._parse_real() - finally: - if self._fd: - self._fd.close() - self._fd = None - - def _parse_real(self): - fd = XFile(self.rarfile) - self._fd = fd - id = fd.read(len(RAR_ID)) - if id != RAR_ID: - raise NotRarFile("Not a Rar archive: " + self.rarfile) - - volume = 0 # first vol (.rar) is 0 - more_vols = 0 - endarc = 0 - volfile = self.rarfile - self._vol_list = [self.rarfile] - while 1: - if endarc: - h = None # don't read past ENDARC - else: - h = self._parse_header(fd) - if not h: - if more_vols: - volume += 1 - fd.close() - try: - volfile = self._next_volname(volfile) - fd = XFile(volfile) - except IOError: - self._set_error("Cannot open next volume: %s", volfile) - break - self._fd = fd - more_vols = 0 - endarc = 0 - self._vol_list.append(volfile) - continue - break - h.volume = volume - h.volume_file = volfile - - if h.type == RAR_BLOCK_MAIN and not self._main: - self._main = h - if h.flags & RAR_MAIN_NEWNUMBERING: - # RAR 2.x does not set FIRSTVOLUME, - # so check it only if NEWNUMBERING is used - if (h.flags & RAR_MAIN_FIRSTVOLUME) == 0: - raise NeedFirstVolume("Need to start from first volume") - if h.flags & RAR_MAIN_PASSWORD: - self._needs_password = True - if not self._password: - self._main = None - break - elif h.type == RAR_BLOCK_ENDARC: - more_vols = h.flags & RAR_ENDARC_NEXT_VOLUME - endarc = 1 - elif h.type == RAR_BLOCK_FILE: - # RAR 2.x does not write RAR_BLOCK_ENDARC - if h.flags & RAR_FILE_SPLIT_AFTER: - more_vols = 1 - # RAR 2.x does not set RAR_MAIN_FIRSTVOLUME - if volume == 0 and h.flags & RAR_FILE_SPLIT_BEFORE: - raise NeedFirstVolume("Need to start from first volume") - - # store it - self._process_entry(h) - - # go to next header - if h.add_size > 0: - fd.seek(h.file_offset + h.add_size, 0) - - # AES encrypted headers - _last_aes_key = (None, None, None) # (salt, key, iv) - - def _decrypt_header(self, fd): - if not _have_crypto: - raise NoCrypto("Cannot parse encrypted headers - no crypto") - salt = fd.read(8) - if self._last_aes_key[0] == salt: - key, iv = self._last_aes_key[1:] - else: - key, iv = rar3_s2k(self._password, salt) - self._last_aes_key = (salt, key, iv) - return HeaderDecrypt(fd, key, iv) - - # read single header - def _parse_header(self, fd): - try: - # handle encrypted headers - if self._main and self._main.flags & RAR_MAIN_PASSWORD: - if not self._password: - return - fd = self._decrypt_header(fd) - - # now read actual header - return self._parse_block_header(fd) - except struct.error: - self._set_error("Broken header in RAR file") - return None - - # common header - def _parse_block_header(self, fd): - h = RarInfo() - h.header_offset = fd.tell() - h.comment = None - - # read and parse base header - buf = fd.read(S_BLK_HDR.size) - if not buf: - return None - t = S_BLK_HDR.unpack_from(buf) - h.header_crc, h.type, h.flags, h.header_size = t - h.header_base = S_BLK_HDR.size - pos = S_BLK_HDR.size - - # read full header - if h.header_size > S_BLK_HDR.size: - h.header_data = buf + fd.read(h.header_size - S_BLK_HDR.size) - else: - h.header_data = buf - h.file_offset = fd.tell() - - # unexpected EOF? - if len(h.header_data) != h.header_size: - self._set_error("Unexpected EOF when reading header") - return None - - # block has data assiciated with it? - if h.flags & RAR_LONG_BLOCK: - h.add_size = S_LONG.unpack_from(h.header_data, pos)[0] - else: - h.add_size = 0 - - # parse interesting ones, decide header boundaries for crc - if h.type == RAR_BLOCK_MARK: - return h - elif h.type == RAR_BLOCK_MAIN: - h.header_base += 6 - if h.flags & RAR_MAIN_ENCRYPTVER: - h.header_base += 1 - if h.flags & RAR_MAIN_COMMENT: - self._parse_subblocks(h, h.header_base) - self.comment = h.comment - elif h.type == RAR_BLOCK_FILE: - self._parse_file_header(h, pos) - elif h.type == RAR_BLOCK_SUB: - self._parse_file_header(h, pos) - h.header_base = h.header_size - elif h.type == RAR_BLOCK_OLD_AUTH: - h.header_base += 8 - elif h.type == RAR_BLOCK_OLD_EXTRA: - h.header_base += 7 - else: - h.header_base = h.header_size - - # check crc - if h.type == RAR_BLOCK_OLD_SUB: - crcdat = h.header_data[2:] + fd.read(h.add_size) - else: - crcdat = h.header_data[2 : h.header_base] - - calc_crc = crc32(crcdat) & 0xFFFF - - # return good header - if h.header_crc == calc_crc: - return h - - # header parsing failed. - self._set_error( - "Header CRC error (%02x): exp=%x got=%x (xlen = %d)", h.type, h.header_crc, calc_crc, len(crcdat) - ) - - # instead panicing, send eof - return None - - # read file-specific header - def _parse_file_header(self, h, pos): - fld = S_FILE_HDR.unpack_from(h.header_data, pos) - h.compress_size = fld[0] - h.file_size = fld[1] - h.host_os = fld[2] - h.CRC = fld[3] - h.date_time = parse_dos_time(fld[4]) - h.extract_version = fld[5] - h.compress_type = fld[6] - h.name_size = fld[7] - h.mode = fld[8] - pos += S_FILE_HDR.size - - if h.flags & RAR_FILE_LARGE: - h1 = S_LONG.unpack_from(h.header_data, pos)[0] - h2 = S_LONG.unpack_from(h.header_data, pos + 4)[0] - h.compress_size |= h1 << 32 - h.file_size |= h2 << 32 - pos += 8 - h.add_size = h.compress_size - - name = h.header_data[pos : pos + h.name_size] - pos += h.name_size - if h.flags & RAR_FILE_UNICODE: - nul = name.find(ZERO) - h.orig_filename = name[:nul] - u = UnicodeFilename(h.orig_filename, name[nul + 1 :]) - h.filename = u.decode() - - # if parsing failed fall back to simple name - if u.failed: - h.filename = self._decode(h.orig_filename) - else: - h.orig_filename = name - h.filename = self._decode(name) - - # change separator, if requested - if PATH_SEP != "\\": - h.filename = h.filename.replace("\\", PATH_SEP) - - if h.flags & RAR_FILE_SALT: - h.salt = h.header_data[pos : pos + 8] - pos += 8 - else: - h.salt = None - - # optional extended time stamps - if h.flags & RAR_FILE_EXTTIME: - pos = self._parse_ext_time(h, pos) - else: - h.mtime = h.atime = h.ctime = h.arctime = None - - # base header end - h.header_base = pos - - if h.flags & RAR_FILE_COMMENT: - self._parse_subblocks(h, pos) - - # convert timestamps - if USE_DATETIME: - h.date_time = to_datetime(h.date_time) - h.mtime = to_datetime(h.mtime) - h.atime = to_datetime(h.atime) - h.ctime = to_datetime(h.ctime) - h.arctime = to_datetime(h.arctime) - - # .mtime is .date_time with more precision - if h.mtime: - if USE_DATETIME: - h.date_time = h.mtime - else: - # keep seconds int - h.date_time = h.mtime[:5] + (int(h.mtime[5]),) - - return pos - - # find old-style comment subblock - def _parse_subblocks(self, h, pos): - hdata = h.header_data - while pos < len(hdata): - # ordinary block header - t = S_BLK_HDR.unpack_from(hdata, pos) - scrc, stype, sflags, slen = t - pos_next = pos + slen - pos += S_BLK_HDR.size - - # corrupt header - if pos_next < pos: - break - - # followed by block-specific header - if stype == RAR_BLOCK_OLD_COMMENT and pos + S_COMMENT_HDR.size <= pos_next: - declen, ver, meth, crc = S_COMMENT_HDR.unpack_from(hdata, pos) - pos += S_COMMENT_HDR.size - data = hdata[pos:pos_next] - cmt = rar_decompress(ver, meth, data, declen, sflags, crc, self._password) - if not self._crc_check: - h.comment = self._decode_comment(cmt) - elif crc32(cmt) & 0xFFFF == crc: - h.comment = self._decode_comment(cmt) - - pos = pos_next - - def _parse_ext_time(self, h, pos): - data = h.header_data - - # flags and rest of data can be missing - flags = 0 - if pos + 2 <= len(data): - flags = S_SHORT.unpack_from(data, pos)[0] - pos += 2 - - h.mtime, pos = self._parse_xtime(flags >> 3 * 4, data, pos, h.date_time) - h.ctime, pos = self._parse_xtime(flags >> 2 * 4, data, pos) - h.atime, pos = self._parse_xtime(flags >> 1 * 4, data, pos) - h.arctime, pos = self._parse_xtime(flags >> 0 * 4, data, pos) - return pos - - def _parse_xtime(self, flag, data, pos, dostime=None): - unit = 10000000.0 # 100 ns units - if flag & 8: - if not dostime: - t = S_LONG.unpack_from(data, pos)[0] - dostime = parse_dos_time(t) - pos += 4 - rem = 0 - cnt = flag & 3 - for i in range(cnt): - b = S_BYTE.unpack_from(data, pos)[0] - rem = (b << 16) | (rem >> 8) - pos += 1 - sec = dostime[5] + rem / unit - if flag & 4: - sec += 1 - dostime = dostime[:5] + (sec,) - return dostime, pos - - # given current vol name, construct next one - def _next_volname(self, volfile): - if is_filelike(volfile): - raise IOError("Working on single FD") - if self._main.flags & RAR_MAIN_NEWNUMBERING: - return self._next_newvol(volfile) - return self._next_oldvol(volfile) - - # new-style next volume - def _next_newvol(self, volfile): - i = len(volfile) - 1 - while i >= 0: - if volfile[i] >= "0" and volfile[i] <= "9": - return self._inc_volname(volfile, i) - i -= 1 - raise BadRarName("Cannot construct volume name: " + volfile) - - # old-style next volume - def _next_oldvol(self, volfile): - # rar -> r00 - if volfile[-4:].lower() == ".rar": - return volfile[:-2] + "00" - return self._inc_volname(volfile, len(volfile) - 1) - - # increase digits with carry, otherwise just increment char - def _inc_volname(self, volfile, i): - fn = list(volfile) - while i >= 0: - if fn[i] != "9": - fn[i] = chr(ord(fn[i]) + 1) - break - fn[i] = "0" - i -= 1 - return "".join(fn) - - def _open_clear(self, inf): - return DirectReader(self, inf) - - # put file compressed data into temporary .rar archive, and run - # unrar on that, thus avoiding unrar going over whole archive - def _open_hack(self, inf, psw=None): - BSIZE = 32 * 1024 - - size = inf.compress_size + inf.header_size - rf = XFile(inf.volume_file, 0) - rf.seek(inf.header_offset) - - tmpfd, tmpname = mkstemp(suffix=".rar") - tmpf = os.fdopen(tmpfd, "wb") - - try: - # create main header: crc, type, flags, size, res1, res2 - mh = S_BLK_HDR.pack(0x90CF, 0x73, 0, 13) + ZERO * (2 + 4) - tmpf.write(RAR_ID + mh) - while size > 0: - if size > BSIZE: - buf = rf.read(BSIZE) - else: - buf = rf.read(size) - if not buf: - raise BadRarFile("read failed: " + inf.filename) - tmpf.write(buf) - size -= len(buf) - tmpf.close() - rf.close() - except: - rf.close() - tmpf.close() - os.unlink(tmpname) - raise - - return self._open_unrar(tmpname, inf, psw, tmpname) - - def _read_comment_v3(self, inf, psw=None): - - # read data - rf = XFile(inf.volume_file) - rf.seek(inf.file_offset) - data = rf.read(inf.compress_size) - rf.close() - - # decompress - cmt = rar_decompress( - inf.extract_version, inf.compress_type, data, inf.file_size, inf.flags, inf.CRC, psw, inf.salt - ) - - # check crc - if self._crc_check: - crc = crc32(cmt) - if crc < 0: - crc += long(1) << 32 - if crc != inf.CRC: - return None - - return self._decode_comment(cmt) - - # extract using unrar - def _open_unrar(self, rarfile, inf, psw=None, tmpfile=None): - if is_filelike(rarfile): - raise ValueError("Cannot use unrar directly on memory buffer") - cmd = [UNRAR_TOOL] + list(OPEN_ARGS) - add_password_arg(cmd, psw) - cmd.append("--") - cmd.append(rarfile) - - # not giving filename avoids encoding related problems - if not tmpfile: - fn = inf.filename - if PATH_SEP != os.sep: - fn = fn.replace(PATH_SEP, os.sep) - cmd.append(fn) - - # read from unrar pipe - return PipeReader(self, inf, cmd, tmpfile) - - def _decode(self, val): - for c in TRY_ENCODINGS: - try: - return val.decode(c) - except UnicodeError: - pass - return val.decode(self._charset, "replace") - - def _decode_comment(self, val): - if UNICODE_COMMENTS: - return self._decode(val) - return val - - # call unrar to extract a file - def _extract(self, fnlist, path=None, psw=None): - cmd = [UNRAR_TOOL] + list(EXTRACT_ARGS) - - # pasoword - psw = psw or self._password - add_password_arg(cmd, psw) - - # rar file - cmd.append(self.rarfile) - - # file list - for fn in fnlist: - if os.sep != PATH_SEP: - fn = fn.replace(PATH_SEP, os.sep) - cmd.append(fn) - - # destination path - if path is not None: - cmd.append(path + os.sep) - - # call - p = custom_popen(cmd) - output = p.communicate()[0] - check_returncode(p, output) - - -## -## Utility classes -## - - -class UnicodeFilename: - """Handle unicode filename decompression""" - - def __init__(self, name, encdata): - self.std_name = bytearray(name) - self.encdata = bytearray(encdata) - self.pos = self.encpos = 0 - self.buf = bytearray() - self.failed = 0 - - def enc_byte(self): - try: - c = self.encdata[self.encpos] - self.encpos += 1 - return c - except IndexError: - self.failed = 1 - return 0 - - def std_byte(self): - try: - return self.std_name[self.pos] - except IndexError: - self.failed = 1 - return ord("?") - - def put(self, lo, hi): - self.buf.append(lo) - self.buf.append(hi) - self.pos += 1 - - def decode(self): - hi = self.enc_byte() - flagbits = 0 - while self.encpos < len(self.encdata): - if flagbits == 0: - flags = self.enc_byte() - flagbits = 8 - flagbits -= 2 - t = (flags >> flagbits) & 3 - if t == 0: - self.put(self.enc_byte(), 0) - elif t == 1: - self.put(self.enc_byte(), hi) - elif t == 2: - self.put(self.enc_byte(), self.enc_byte()) - else: - n = self.enc_byte() - if n & 0x80: - c = self.enc_byte() - for i in range((n & 0x7F) + 2): - lo = (self.std_byte() + c) & 0xFF - self.put(lo, hi) - else: - for i in range(n + 2): - self.put(self.std_byte(), 0) - return self.buf.decode("utf-16le", "replace") - - -class RarExtFile(RawIOBase): - """Base class for file-like object that :meth:`RarFile.open` returns. - - Provides public methods and common crc checking. - - Behaviour: - - no short reads - .read() and .readinfo() read as much as requested. - - no internal buffer, use io.BufferedReader for that. - - If :mod:`io` module is available (Python 2.6+, 3.x), then this calls - will inherit from :class:`io.RawIOBase` class. This makes line-based - access available: :meth:`RarExtFile.readline` and ``for ln in f``. - """ - - #: Filename of the archive entry - name = None - - def __init__(self, rf, inf): - RawIOBase.__init__(self) - - # standard io.* properties - self.name = inf.filename - self.mode = "rb" - - self.rf = rf - self.inf = inf - self.crc_check = rf._crc_check - self.fd = None - self.CRC = 0 - self.remain = 0 - self.returncode = 0 - - self._open() - - def _open(self): - if self.fd: - self.fd.close() - self.fd = None - self.CRC = 0 - self.remain = self.inf.file_size - - def read(self, cnt=None): - """Read all or specified amount of data from archive entry.""" - - # sanitize cnt - if cnt is None or cnt < 0: - cnt = self.remain - elif cnt > self.remain: - cnt = self.remain - if cnt == 0: - return EMPTY - - # actual read - data = self._read(cnt) - if data: - self.CRC = crc32(data, self.CRC) - self.remain -= len(data) - if len(data) != cnt: - raise BadRarFile("Failed the read enough data") - - # done? - if not data or self.remain == 0: - # self.close() - self._check() - return data - - def _check(self): - """Check final CRC.""" - if not self.crc_check: - return - if self.returncode: - check_returncode(self, "") - if self.remain != 0: - raise BadRarFile("Failed the read enough data") - crc = self.CRC - if crc < 0: - crc += long(1) << 32 - if crc != self.inf.CRC: - raise BadRarFile("Corrupt file - CRC check failed: " + self.inf.filename) - - def _read(self, cnt): - """Actual read that gets sanitized cnt.""" - - def close(self): - """Close open resources.""" - - RawIOBase.close(self) - - if self.fd: - self.fd.close() - self.fd = None - - def __del__(self): - """Hook delete to make sure tempfile is removed.""" - self.close() - - def readinto(self, buf): - """Zero-copy read directly into buffer. - - Returns bytes read. - """ - - data = self.read(len(buf)) - n = len(data) - try: - buf[:n] = data - except TypeError: - import array - - if not isinstance(buf, array.array): - raise - buf[:n] = array.array(buf.typecode, data) - return n - - def tell(self): - """Return current reading position in uncompressed data.""" - return self.inf.file_size - self.remain - - def seek(self, ofs, whence=0): - """Seek in data. - - On uncompressed files, the seeking works by actual - seeks so it's fast. On compresses files its slow - - forward seeking happends by reading ahead, - backwards by re-opening and decompressing from the start. - """ - - # disable crc check when seeking - self.crc_check = 0 - - fsize = self.inf.file_size - cur_ofs = self.tell() - - if whence == 0: # seek from beginning of file - new_ofs = ofs - elif whence == 1: # seek from current position - new_ofs = cur_ofs + ofs - elif whence == 2: # seek from end of file - new_ofs = fsize + ofs - else: - raise ValueError("Invalid value for whence") - - # sanity check - if new_ofs < 0: - new_ofs = 0 - elif new_ofs > fsize: - new_ofs = fsize - - # do the actual seek - if new_ofs >= cur_ofs: - self._skip(new_ofs - cur_ofs) - else: - # process old data ? - # self._skip(fsize - cur_ofs) - # reopen and seek - self._open() - self._skip(new_ofs) - return self.tell() - - def _skip(self, cnt): - """Read and discard data""" - while cnt > 0: - if cnt > 8192: - buf = self.read(8192) - else: - buf = self.read(cnt) - if not buf: - break - cnt -= len(buf) - - def readable(self): - """Returns True""" - return True - - def writable(self): - """Returns False. - - Writing is not supported.""" - return False - - def seekable(self): - """Returns True. - - Seeking is supported, although it's slow on compressed files. - """ - return True - - def readall(self): - """Read all remaining data""" - # avoid RawIOBase default impl - return self.read() - - -class PipeReader(RarExtFile): - """Read data from pipe, handle tempfile cleanup.""" - - def __init__(self, rf, inf, cmd, tempfile=None): - self.cmd = cmd - self.proc = None - self.tempfile = tempfile - RarExtFile.__init__(self, rf, inf) - - def _close_proc(self): - if not self.proc: - return - if self.proc.stdout: - self.proc.stdout.close() - if self.proc.stdin: - self.proc.stdin.close() - if self.proc.stderr: - self.proc.stderr.close() - self.proc.wait() - self.returncode = self.proc.returncode - self.proc = None - - def _open(self): - RarExtFile._open(self) - - # stop old process - self._close_proc() - - # launch new process - self.returncode = 0 - self.proc = custom_popen(self.cmd) - self.fd = self.proc.stdout - - # avoid situation where unrar waits on stdin - if self.proc.stdin: - self.proc.stdin.close() - - def _read(self, cnt): - """Read from pipe.""" - - # normal read is usually enough - data = self.fd.read(cnt) - if len(data) == cnt or not data: - return data - - # short read, try looping - buf = [data] - cnt -= len(data) - while cnt > 0: - data = self.fd.read(cnt) - if not data: - break - cnt -= len(data) - buf.append(data) - return EMPTY.join(buf) - - def close(self): - """Close open resources.""" - - self._close_proc() - RarExtFile.close(self) - - if self.tempfile: - try: - os.unlink(self.tempfile) - except OSError: - pass - self.tempfile = None - - if have_memoryview: - - def readinto(self, buf): - """Zero-copy read directly into buffer.""" - cnt = len(buf) - if cnt > self.remain: - cnt = self.remain - vbuf = memoryview(buf) - res = got = 0 - while got < cnt: - res = self.fd.readinto(vbuf[got:cnt]) - if not res: - break - if self.crc_check: - self.CRC = crc32(vbuf[got : got + res], self.CRC) - self.remain -= res - got += res - return got - - -class DirectReader(RarExtFile): - """Read uncompressed data directly from archive.""" - - def _open(self): - RarExtFile._open(self) - - self.volfile = self.inf.volume_file - self.fd = XFile(self.volfile, 0) - self.fd.seek(self.inf.header_offset, 0) - self.cur = self.rf._parse_header(self.fd) - self.cur_avail = self.cur.add_size - - def _skip(self, cnt): - """RAR Seek, skipping through rar files to get to correct position - """ - - while cnt > 0: - # next vol needed? - if self.cur_avail == 0: - if not self._open_next(): - break - - # fd is in read pos, do the read - if cnt > self.cur_avail: - cnt -= self.cur_avail - self.remain -= self.cur_avail - self.cur_avail = 0 - else: - self.fd.seek(cnt, 1) - self.cur_avail -= cnt - self.remain -= cnt - cnt = 0 - - def _read(self, cnt): - """Read from potentially multi-volume archive.""" - - buf = [] - while cnt > 0: - # next vol needed? - if self.cur_avail == 0: - if not self._open_next(): - break - - # fd is in read pos, do the read - if cnt > self.cur_avail: - data = self.fd.read(self.cur_avail) - else: - data = self.fd.read(cnt) - if not data: - break - - # got some data - cnt -= len(data) - self.cur_avail -= len(data) - buf.append(data) - - if len(buf) == 1: - return buf[0] - return EMPTY.join(buf) - - def _open_next(self): - """Proceed to next volume.""" - - # is the file split over archives? - if (self.cur.flags & RAR_FILE_SPLIT_AFTER) == 0: - return False - - if self.fd: - self.fd.close() - self.fd = None - - # open next part - self.volfile = self.rf._next_volname(self.volfile) - fd = open(self.volfile, "rb", 0) - self.fd = fd - - # loop until first file header - while 1: - cur = self.rf._parse_header(fd) - if not cur: - raise BadRarFile("Unexpected EOF") - if cur.type in (RAR_BLOCK_MARK, RAR_BLOCK_MAIN): - if cur.add_size: - fd.seek(cur.add_size, 1) - continue - if cur.orig_filename != self.inf.orig_filename: - raise BadRarFile("Did not found file entry") - self.cur = cur - self.cur_avail = cur.add_size - return True - - if have_memoryview: - - def readinto(self, buf): - """Zero-copy read directly into buffer.""" - got = 0 - vbuf = memoryview(buf) - while got < len(buf): - # next vol needed? - if self.cur_avail == 0: - if not self._open_next(): - break - - # lenght for next read - cnt = len(buf) - got - if cnt > self.cur_avail: - cnt = self.cur_avail - - # read into temp view - res = self.fd.readinto(vbuf[got : got + cnt]) - if not res: - break - if self.crc_check: - self.CRC = crc32(vbuf[got : got + res], self.CRC) - self.cur_avail -= res - self.remain -= res - got += res - return got - - -class HeaderDecrypt: - """File-like object that decrypts from another file""" - - def __init__(self, f, key, iv): - self.f = f - self.ciph = AES.new(key, AES.MODE_CBC, iv) - self.buf = EMPTY - - def tell(self): - return self.f.tell() - - def read(self, cnt=None): - if cnt > 8 * 1024: - raise BadRarFile("Bad count to header decrypt - wrong password?") - - # consume old data - if cnt <= len(self.buf): - res = self.buf[:cnt] - self.buf = self.buf[cnt:] - return res - res = self.buf - self.buf = EMPTY - cnt -= len(res) - - # decrypt new data - BLK = self.ciph.block_size - while cnt > 0: - enc = self.f.read(BLK) - if len(enc) < BLK: - break - dec = self.ciph.decrypt(enc) - if cnt >= len(dec): - res += dec - cnt -= len(dec) - else: - res += dec[:cnt] - self.buf = dec[cnt:] - cnt = 0 - - return res - - -# handle (filename|filelike) object -class XFile(object): - __slots__ = ("_fd", "_need_close") - - def __init__(self, xfile, bufsize=1024): - if is_filelike(xfile): - self._need_close = False - self._fd = xfile - self._fd.seek(0) - else: - self._need_close = True - self._fd = open(xfile, "rb", bufsize) - - def read(self, n=None): - return self._fd.read(n) - - def tell(self): - return self._fd.tell() - - def seek(self, ofs, whence=0): - return self._fd.seek(ofs, whence) - - def readinto(self, dst): - return self._fd.readinto(dst) - - def close(self): - if self._need_close: - self._fd.close() - - def __enter__(self): - return self - - def __exit__(self, typ, val, tb): - self.close() - - -## -## Utility functions -## - - -def is_filelike(obj): - if isinstance(obj, str) or isinstance(obj, unicode): - return False - res = True - for a in ("read", "tell", "seek"): - res = res and hasattr(obj, a) - if not res: - raise ValueError("Invalid object passed as file") - return True - - -def rar3_s2k(psw, salt): - """String-to-key hash for RAR3.""" - - seed = psw.encode("utf-16le") + salt - iv = EMPTY - h = sha1() - for i in range(16): - for j in range(0x4000): - cnt = S_LONG.pack(i * 0x4000 + j) - h.update(seed + cnt[:3]) - if j == 0: - iv += h.digest()[19:20] - key_be = h.digest()[:16] - key_le = pack("LLLL", key_be)) - return key_le, iv - - -def rar_decompress(vers, meth, data, declen=0, flags=0, crc=0, psw=None, salt=None): - """Decompress blob of compressed data. - - Used for data with non-standard header - eg. comments. - """ - - # already uncompressed? - if meth == RAR_M0 and (flags & RAR_FILE_PASSWORD) == 0: - return data - - # take only necessary flags - flags = flags & (RAR_FILE_PASSWORD | RAR_FILE_SALT | RAR_FILE_DICTMASK) - flags |= RAR_LONG_BLOCK - - # file header - fname = bytes("data", "ascii") - date = 0 - mode = 0x20 - fhdr = S_FILE_HDR.pack(len(data), declen, RAR_OS_MSDOS, crc, date, vers, meth, len(fname), mode) - fhdr += fname - if flags & RAR_FILE_SALT: - if not salt: - return EMPTY - fhdr += salt - - # full header - hlen = S_BLK_HDR.size + len(fhdr) - hdr = S_BLK_HDR.pack(0, RAR_BLOCK_FILE, flags, hlen) + fhdr - hcrc = crc32(hdr[2:]) & 0xFFFF - hdr = S_BLK_HDR.pack(hcrc, RAR_BLOCK_FILE, flags, hlen) + fhdr - - # archive main header - mh = S_BLK_HDR.pack(0x90CF, RAR_BLOCK_MAIN, 0, 13) + ZERO * (2 + 4) - - # decompress via temp rar - tmpfd, tmpname = mkstemp(suffix=".rar") - tmpf = os.fdopen(tmpfd, "wb") - try: - tmpf.write(RAR_ID + mh + hdr + data) - tmpf.close() - - cmd = [UNRAR_TOOL] + list(OPEN_ARGS) - add_password_arg(cmd, psw, (flags & RAR_FILE_PASSWORD)) - cmd.append(tmpname) - - p = custom_popen(cmd) - return p.communicate()[0] - finally: - tmpf.close() - os.unlink(tmpname) - - -def to_datetime(t): - """Convert 6-part time tuple into datetime object.""" - - if t is None: - return None - - # extract values - year, mon, day, h, m, xs = t - s = int(xs) - us = int(1000000 * (xs - s)) - - # assume the values are valid - try: - return datetime(year, mon, day, h, m, s, us) - except ValueError: - pass - - # sanitize invalid values - MDAY = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - if mon < 1: - mon = 1 - if mon > 12: - mon = 12 - if day < 1: - day = 1 - if day > MDAY[mon]: - day = MDAY[mon] - if h > 23: - h = 23 - if m > 59: - m = 59 - if s > 59: - s = 59 - if mon == 2 and day == 29: - try: - return datetime(year, mon, day, h, m, s, us) - except ValueError: - day = 28 - return datetime(year, mon, day, h, m, s, us) - - -def parse_dos_time(stamp): - """Parse standard 32-bit DOS timestamp.""" - - sec = stamp & 0x1F - stamp = stamp >> 5 - min = stamp & 0x3F - stamp = stamp >> 6 - hr = stamp & 0x1F - stamp = stamp >> 5 - day = stamp & 0x1F - stamp = stamp >> 5 - mon = stamp & 0x0F - stamp = stamp >> 4 - yr = (stamp & 0x7F) + 1980 - return (yr, mon, day, hr, min, sec * 2) - - -def custom_popen(cmd): - """Disconnect cmd from parent fds, read only from stdout.""" - - # needed for py2exe - creationflags = 0 - if sys.platform == "win32": - creationflags = 0x08000000 # CREATE_NO_WINDOW - - # run command - try: - p = Popen(cmd, bufsize=0, stdout=PIPE, stdin=PIPE, stderr=STDOUT, creationflags=creationflags) - except OSError: - ex = sys.exc_info()[1] - if ex.errno == errno.ENOENT: - raise RarCannotExec("Unrar not installed? (rarfile.UNRAR_TOOL=%r)" % UNRAR_TOOL) - raise - return p - - -def custom_check(cmd, ignore_retcode=False): - """Run command, collect output, raise error if needed.""" - p = custom_popen(cmd) - out, err = p.communicate() - if p.returncode and not ignore_retcode: - raise RarExecError("Check-run failed") - return out - - -def add_password_arg(cmd, psw, required=False): - """Append password switch to commandline.""" - if UNRAR_TOOL == ALT_TOOL: - return - if psw is not None: - cmd.append("-p" + psw) - else: - cmd.append("-p-") - - -def check_returncode(p, out): - """Raise exception according to unrar exit code""" - - code = p.returncode - if code == 0: - return - - # map return code to exception class - errmap = [ - None, - RarWarning, - RarFatalError, - RarCRCError, - RarLockedArchiveError, - RarWriteError, - RarOpenError, - RarUserError, - RarMemoryError, - RarCreateError, - RarNoFilesError, - ] # codes from rar.txt - if UNRAR_TOOL == ALT_TOOL: - errmap = [None] - if code > 0 and code < len(errmap): - exc = errmap[code] - elif code == 255: - exc = RarUserBreak - elif code < 0: - exc = RarSignalExit - else: - exc = RarUnknownError - - # format message - if out: - msg = "%s [%d]: %s" % (exc.__doc__, p.returncode, out) - else: - msg = "%s [%d]" % (exc.__doc__, p.returncode) - - raise exc(msg) - - -# -# Check if unrar works -# - -try: - # does UNRAR_TOOL work? - custom_check([UNRAR_TOOL], True) -except RarCannotExec: - try: - # does ALT_TOOL work? - custom_check([ALT_TOOL] + list(ALT_CHECK_ARGS), True) - # replace config - UNRAR_TOOL = ALT_TOOL - OPEN_ARGS = ALT_OPEN_ARGS - EXTRACT_ARGS = ALT_EXTRACT_ARGS - TEST_ARGS = ALT_TEST_ARGS - except RarCannotExec: - # no usable tool, only uncompressed archives work - pass diff --git a/comic/test/test_folder/blank.txt b/comic/tests/__init__.py similarity index 100% rename from comic/test/test_folder/blank.txt rename to comic/tests/__init__.py diff --git a/comic/tests/test_models.py b/comic/tests/test_models.py index f162bf1..1fa4907 100644 --- a/comic/tests/test_models.py +++ b/comic/tests/test_models.py @@ -1,10 +1,12 @@ import json import os -from os import path +# from os import path from django.contrib.auth.models import User from django.test import Client, TestCase from django.utils.http import urlsafe_base64_encode +from django.conf import settings +from pathlib import Path from comic.models import ComicBook, ComicPage, ComicStatus, Directory, Setting from comic.util import generate_directory @@ -12,7 +14,7 @@ from comic.util import generate_directory class ComicBookTests(TestCase): def setUp(self): - Setting.objects.create(name="BASE_DIR", value=path.join(os.getcwd(), "comic", "test")) + settings.COMIC_BOOK_VOLUME = Path(Path.cwd(), 'test_comics') User.objects.create_user("test", "test@test.com", "test") user = User.objects.first() ComicBook.process_comic_book("test1.rar") @@ -53,73 +55,48 @@ class ComicBookTests(TestCase): self.assertEqual(content_type, "image/jpeg") self.assertEqual(img.read(), b"img1.jpg") - def test_nav_first_page_with_folder_above(self): + def test_nav_with_folder_above(self): + user = User.objects.get(username="test") + generate_directory(user) book = ComicBook.objects.get(file_name="test1.rar") - user = User.objects.get(username="test") - nav = book.nav(0, user) - self.assertEqual(nav.next_index, 1) - self.assertEqual(nav.next_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.prev_index, -1) + + nav = book.nav(user) + self.assertEqual(nav.prev_path, "") - self.assertEqual(nav.cur_index, 0) self.assertEqual(nav.cur_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.q_prev_to_directory, True) - self.assertEqual(nav.q_next_to_directory, False) - def test_nav_first_page_with_comic_above(self): - prev_book = ComicBook.objects.get(file_name="test1.rar") + def test_nav_with_comic_above(self): + user = User.objects.get(username="test") + generate_directory(user) + prev_book = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True) book = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True) - user = User.objects.get(username="test") + next_book = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True) + + + nav = book.nav(user) - nav = book.nav(0, user) - self.assertEqual(nav.next_index, 1) - self.assertEqual(nav.next_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.prev_index, 0) self.assertEqual(nav.prev_path, urlsafe_base64_encode(prev_book.selector.bytes)) - self.assertEqual(nav.cur_index, 0) self.assertEqual(nav.cur_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.q_prev_to_directory, False) - self.assertEqual(nav.q_next_to_directory, False) + self.assertEqual(nav.next_path, urlsafe_base64_encode(next_book.selector.bytes)) - def test_nav_last_page_with_comic_below(self): + def test_nav_with_comic_below(self): user = User.objects.get(username="test") + generate_directory(user) book = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True) next_book = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True) - nav = book.nav(3, user) - self.assertEqual(nav.next_index, 2) + nav = book.nav(user) + + self.assertEqual(nav.cur_path, urlsafe_base64_encode(book.selector.bytes)) self.assertEqual(nav.next_path, urlsafe_base64_encode(next_book.selector.bytes)) - self.assertEqual(nav.prev_index, 2) - self.assertEqual(nav.prev_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.cur_index, 3) - self.assertEqual(nav.cur_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.q_prev_to_directory, False) - self.assertEqual(nav.q_next_to_directory, False) - def test_nav_last_page_with_nothing_below(self): + def test_nav_with_nothing_below(self): user = User.objects.get(username="test") + generate_directory(user) book = ComicBook.objects.get(file_name="test4.rar") - nav = book.nav(3, user) - self.assertEqual(nav.next_index, -1) - self.assertEqual(nav.next_path, "") - self.assertEqual(nav.prev_index, 2) - self.assertEqual(nav.prev_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.cur_index, 3) - self.assertEqual(nav.cur_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.q_prev_to_directory, False) - self.assertEqual(nav.q_next_to_directory, True) + nav = book.nav(user) - def test_nav_in_comic(self): - user = User.objects.get(username="test") - book = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True) - nav = book.nav(1, user) - self.assertEqual(nav.next_index, 2) - self.assertEqual(nav.next_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.prev_index, 0) - self.assertEqual(nav.prev_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.cur_index, 1) self.assertEqual(nav.cur_path, urlsafe_base64_encode(book.selector.bytes)) - self.assertEqual(nav.q_prev_to_directory, False) - self.assertEqual(nav.q_next_to_directory, False) + self.assertEqual(nav.next_path, "") def test_generate_directory(self): user = User.objects.get(username="test") @@ -127,7 +104,7 @@ class ComicBookTests(TestCase): dir1 = folders[0] self.assertEqual(dir1.name, "test_folder") self.assertEqual(dir1.type, "directory") - self.assertEqual(dir1.icon, "glyphicon-folder-open") + self.assertEqual("fa-folder-open", dir1.icon) d = Directory.objects.get(name="test_folder", parent__isnull=True) location = "/comic/{0}/".format(urlsafe_base64_encode(d.selector.bytes)) self.assertEqual(dir1.location, location) @@ -136,27 +113,27 @@ class ComicBookTests(TestCase): dir2 = folders[1] self.assertEqual(dir2.name, "test1.rar") self.assertEqual(dir2.type, "book") - self.assertEqual(dir2.icon, "glyphicon-book") + self.assertEqual("fa-book", dir2.icon) c = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True) - location = "/comic/read/{0}/{1}/".format(urlsafe_base64_encode(c.selector.bytes), "0") + location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes)) self.assertEqual(dir2.location, location) self.assertEqual(dir2.label, '
Unread
') dir3 = folders[2] self.assertEqual(dir3.name, "test2.rar") self.assertEqual(dir3.type, "book") - self.assertEqual(dir3.icon, "glyphicon-book") + self.assertEqual("fa-book", dir3.icon) c = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True) - location = "/comic/read/{0}/{1}/".format(urlsafe_base64_encode(c.selector.bytes), "2") + location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes)) self.assertEqual(dir3.location, location) self.assertEqual(dir3.label, '
3/4
') dir4 = folders[3] self.assertEqual(dir4.name, "test3.rar") self.assertEqual(dir4.type, "book") - self.assertEqual(dir3.icon, "glyphicon-book") + self.assertEqual("fa-book", dir3.icon) c = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True) - location = "/comic/read/{0}/{1}/".format(urlsafe_base64_encode(c.selector.bytes), "0") + location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes)) self.assertEqual(dir4.location, location) self.assertEqual(dir4.label, '
Unread
') @@ -217,8 +194,8 @@ class ComicBookTests(TestCase): self.assertEqual(response.status_code, 302) c.login(username="test", password="test") - generate_directory(User.objects.first()) - ComicStatus.objects.all().delete() + user = User.objects.get(username="test") + folders = generate_directory(user) req_data = {"start": "0", "length": "10", "search[value]": "", "order[0][dir]": "desc"} response = c.post("/comic/recent/json/", req_data) @@ -234,12 +211,12 @@ class ComicBookTests(TestCase): "data": [ { "date": book.date_added.strftime("%d/%m/%y-%H:%M"), - "icon": '', + "icon": '', "label": '
Unread
', "name": "test1.rar", "selector": urlsafe_base64_encode(book.selector.bytes), "type": "book", - "url": f"/comic/read/" f"{urlsafe_base64_encode(book.selector.bytes)}/0/", + "url": f"/comic/read/" f"{urlsafe_base64_encode(book.selector.bytes)}/", } ], "recordsFiltered": 1, @@ -250,17 +227,6 @@ class ComicBookTests(TestCase): req_data["order[0][dir]"] = 3 response = c.post("/comic/recent/json/", req_data) - self.assertListEqual( - [x["name"] for x in json.loads(response.content)["data"]], - ["test1.rar", "test2.rar", "test4.rar", "test3.rar"], - ) - - req_data["order[0][dir]"] = 2 - response = c.post("/comic/recent/json/", req_data) - self.assertListEqual( - [x["name"] for x in json.loads(response.content)["data"]], - ["test1.rar", "test2.rar", "test4.rar", "test3.rar"], - ) def test_comic_edit(self): c = Client() diff --git a/comic/util.py b/comic/util.py index 4f62d87..27ef783 100644 --- a/comic/util.py +++ b/comic/util.py @@ -1,16 +1,18 @@ from collections import OrderedDict -from os import listdir, path +from os import listdir +from pathlib import Path -from django.db.models import Count, Q, F, Max, ExpressionWrapper, Prefetch +from django.conf import settings +from django.db.models import Count, Q, F from django.utils.http import urlsafe_base64_encode -from collections import Counter -from .models import ComicBook, Directory, Setting, ComicStatus + +from .models import ComicBook, Directory, ComicStatus -def generate_title_from_path(file_path): - if file_path == "": +def generate_title_from_path(file_path: Path): + if file_path == "Home": return "CBWebReader" - return "CBWebReader - " + " - ".join(file_path.split(path.sep)) + return f'CBWebReader - {" - ".join(p for p in file_path.parts)}' class Menu: @@ -122,21 +124,23 @@ def generate_directory(user, directory=False): :type user: User :type directory: Directory """ - base_dir = Setting.objects.get(name="BASE_DIR").value + base_dir = settings.COMIC_BOOK_VOLUME files = [] if directory: - ordered_dir_list = listdir(path.join(base_dir, directory.path)) - dir_list = [x for x in ordered_dir_list if path.isdir(path.join(base_dir, directory.path, x))] + dir_path = Path(base_dir, directory.path) + # ordered_dir_list = sorted(dir_path.glob('*')) + dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, directory.path, x).is_dir()] else: - ordered_dir_list = listdir(base_dir) - dir_list = [x for x in ordered_dir_list if path.isdir(path.join(base_dir, x))] - file_list = [x for x in ordered_dir_list if x not in dir_list] + dir_path = base_dir + # ordered_dir_list = base_dir.glob('*') + dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, x).is_dir()] + file_list = [x for x in sorted(dir_path.glob('*')) if x.is_file()] if directory: - dir_list_obj = Directory.objects.filter(name__in=dir_list, parent=directory) - file_list_obj = ComicBook.objects.filter(file_name__in=file_list, directory=directory) + dir_list_obj = Directory.objects.filter(name__in=[x.name for x in dir_list], parent=directory) + file_list_obj = ComicBook.objects.filter(file_name__in=[x.name for x in file_list], directory=directory) else: - dir_list_obj = Directory.objects.filter(name__in=dir_list, parent__isnull=True) - file_list_obj = ComicBook.objects.filter(file_name__in=file_list, directory__isnull=True) + dir_list_obj = Directory.objects.filter(name__in=[x.name for x in dir_list], parent__isnull=True) + file_list_obj = ComicBook.objects.filter(file_name__in=[x.name for x in file_list], directory__isnull=True) dir_list_obj = dir_list_obj.annotate( total=Count('comicbook', distinct=True), @@ -162,19 +166,19 @@ def generate_directory(user, directory=False): df = DirFile() df.populate_directory(directory_obj, user) files.append(df) - dir_list.remove(directory_obj.name) + dir_list.remove(Path(dir_path, directory_obj.name)) for file_obj in file_list_obj: df = DirFile() df.populate_comic(file_obj, user) files.append(df) - file_list.remove(file_obj.file_name) + file_list.remove(Path(dir_path, file_obj.file_name)) for directory_name in dir_list: if directory: - directory_obj = Directory(name=directory_name, parent=directory) + directory_obj = Directory(name=directory_name.name, parent=directory) else: - directory_obj = Directory(name=directory_name) + directory_obj = Directory(name=directory_name.name) directory_obj.save() directory_obj.total = 0 directory_obj.total_read = 0 @@ -183,8 +187,8 @@ def generate_directory(user, directory=False): files.append(df) for file_name in file_list: - if file_name.lower()[-4:] in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]: - book = ComicBook.process_comic_book(file_name, directory) + if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]: + book = ComicBook.process_comic_book(file_name.name, directory) df = DirFile() df.populate_comic(book, user) files.append(df) @@ -226,14 +230,3 @@ def generate_dir_status(total, total_read): elif total_read == 0: return '
Unread
' return f'
{total_read}/{total}
' - - -def get_ordered_dir_list(folder): - directories = [] - files = [] - for item in listdir(folder): - if path.isdir(path.join(folder, item)): - directories.append(item) - else: - files.append(item) - return sorted(directories) + sorted(files) diff --git a/comic/views.py b/comic/views.py index dd4a286..27b148d 100644 --- a/comic/views.py +++ b/comic/views.py @@ -1,9 +1,5 @@ -try: - import ujson as json -except ImportError: - import json +import json import uuid -from os import path from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required, user_passes_test @@ -32,12 +28,14 @@ from .util import ( @ensure_csrf_cookie @login_required def comic_list(request, directory_selector=False): - try: - base_dir = Setting.objects.get(name="BASE_DIR").value - except Setting.DoesNotExist: - return redirect("/comic/settings/") - if not path.isdir(base_dir): + if User.objects.all().count() == 0: return redirect("/comic/settings/") + # try: + # base_dir = Setting.objects.get(name="BASE_DIR").value + # except Setting.DoesNotExist: + # return redirect("/comic/settings/") + # if not path.isdir(base_dir): + # return redirect("/comic/settings/") if directory_selector: selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector)) @@ -311,7 +309,7 @@ def read_comic(request, comic_selector): "book": book, "pages": pages, # "orig_file_name": book.page_name(page), - "nav": book.nav(0, request.user), + "nav": book.nav(request.user), "status": status, "breadcrumbs": generate_breadcrumbs_from_path(book.directory, book), "menu": Menu(request.user), @@ -369,9 +367,6 @@ def initial_setup(request): ) user.set_password(form.cleaned_data["password"]) user.save() - base_dir, _ = Setting.objects.get_or_create(name="BASE_DIR") - base_dir.value = form.cleaned_data["base_dir"] - base_dir.save() user = authenticate(username=form.cleaned_data["username"], password=form.cleaned_data["password"]) login(request, user) return redirect("/comic/") diff --git a/poetry.lock b/poetry.lock index c1ee8c4..d025bbf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -60,6 +60,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + [[package]] name = "dj-database-url" version = "0.5.0" @@ -267,6 +278,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "rarfile" +version = "4.0" +description = "RAR archive reader for Python" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "requests" version = "2.25.1" @@ -344,7 +363,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "cd143483d60bfe34643734cbc6bfcc7b50d3257f0a900fe34b70fd43b1cf0305" +content-hash = "2cf2bf8371f6fa6dcc2a9d8d889387667eed3a994e46797c5113f8ea7e7ffd95" [metadata.files] asgiref = [ @@ -372,6 +391,60 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] dj-database-url = [ {file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"}, {file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"}, @@ -510,6 +583,10 @@ pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +rarfile = [ + {file = "rarfile-4.0-py3-none-any.whl", hash = "sha256:1094869119012f95c31a6f22cc3a9edbdca61861b805241116adbe2d737b68f8"}, + {file = "rarfile-4.0.tar.gz", hash = "sha256:67548769229c5bda0827c1663dce3f54644f9dbfba4ae86d4da2b2afd3e602a1"}, +] requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, diff --git a/pyproject.toml b/pyproject.toml index 4c40b56..154adf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ loguru = "^0.5.3" django-silk = "^4.1.0" mysqlclient = "^2.0.1" psycopg2 = "^2.8.6" +rarfile = "^4.0" +coverage = "^5.5" [tool.poetry.dev-dependencies] diff --git a/comic/test/test1.rar b/test_comics/test1.rar similarity index 100% rename from comic/test/test1.rar rename to test_comics/test1.rar diff --git a/comic/test/test2.rar b/test_comics/test2.rar similarity index 100% rename from comic/test/test2.rar rename to test_comics/test2.rar diff --git a/comic/test/test3.rar b/test_comics/test3.rar similarity index 100% rename from comic/test/test3.rar rename to test_comics/test3.rar diff --git a/comic/test/test4.rar b/test_comics/test4.rar similarity index 100% rename from comic/test/test4.rar rename to test_comics/test4.rar diff --git a/test_comics/test_folder/blank.txt b/test_comics/test_folder/blank.txt new file mode 100644 index 0000000..e69de29