2009-10-22 15:23:32 +00:00
|
|
|
# Created By: Virgil Dupras
|
|
|
|
# Created On: 2009-10-22
|
2015-01-03 21:30:57 +00:00
|
|
|
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
2014-10-13 19:08:59 +00:00
|
|
|
#
|
2015-01-03 21:33:16 +00:00
|
|
|
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
2014-10-13 19:08:59 +00:00
|
|
|
# which should be included with this package. The terms are also available at
|
2015-01-03 21:33:16 +00:00
|
|
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
2009-10-22 15:23:32 +00:00
|
|
|
|
|
|
|
# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru
|
|
|
|
# and was re-used for dupeGuru. The problem is that hsfs is way over-engineered for dupeGuru,
|
|
|
|
# resulting needless complexity and memory usage. It's been a while since I wanted to do that fork,
|
|
|
|
# and I'm doing it now.
|
|
|
|
|
|
|
|
import hashlib
|
2021-06-21 17:03:21 +00:00
|
|
|
from math import floor
|
2009-10-22 15:23:32 +00:00
|
|
|
import logging
|
2021-10-29 04:22:12 +00:00
|
|
|
import sqlite3
|
|
|
|
from threading import Lock
|
|
|
|
from typing import Any
|
2009-10-22 15:23:32 +00:00
|
|
|
|
2021-10-29 04:22:12 +00:00
|
|
|
from hscommon.path import Path
|
2012-05-29 21:39:54 +00:00
|
|
|
from hscommon.util import nonone, get_file_ext
|
|
|
|
|
2013-08-18 22:36:09 +00:00
|
|
|
__all__ = [
|
2020-01-01 02:16:27 +00:00
|
|
|
"File",
|
|
|
|
"Folder",
|
|
|
|
"get_file",
|
|
|
|
"get_files",
|
|
|
|
"FSError",
|
|
|
|
"AlreadyExistsError",
|
|
|
|
"InvalidPath",
|
|
|
|
"InvalidDestinationError",
|
|
|
|
"OperationError",
|
2013-08-18 22:36:09 +00:00
|
|
|
]
|
|
|
|
|
2012-05-29 21:39:54 +00:00
|
|
|
NOT_SET = object()
|
2009-10-22 15:23:32 +00:00
|
|
|
|
2021-06-21 17:03:21 +00:00
|
|
|
# The goal here is to not run out of memory on really big files. However, the chunk
|
|
|
|
# size has to be large enough so that the python loop isn't too costly in terms of
|
|
|
|
# CPU.
|
|
|
|
CHUNK_SIZE = 1024 * 1024 # 1 MiB
|
|
|
|
|
2021-08-13 19:33:21 +00:00
|
|
|
# Minimum size below which partial hashes don't need to be computed
|
|
|
|
MIN_FILE_SIZE = 3 * CHUNK_SIZE # 3MiB, because we take 3 samples
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2021-08-14 00:52:00 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
class FSError(Exception):
|
|
|
|
cls_message = "An error has occured on '{name}' in '{parent}'"
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
def __init__(self, fsobject, parent=None):
|
|
|
|
message = self.cls_message
|
2010-08-11 14:39:06 +00:00
|
|
|
if isinstance(fsobject, str):
|
2009-10-22 15:23:32 +00:00
|
|
|
name = fsobject
|
|
|
|
elif isinstance(fsobject, File):
|
|
|
|
name = fsobject.name
|
|
|
|
else:
|
2020-01-01 02:16:27 +00:00
|
|
|
name = ""
|
|
|
|
parentname = str(parent) if parent is not None else ""
|
2009-10-22 15:23:32 +00:00
|
|
|
Exception.__init__(self, message.format(name=name, parent=parentname))
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
|
|
|
|
class AlreadyExistsError(FSError):
|
|
|
|
"The directory or file name we're trying to add already exists"
|
|
|
|
cls_message = "'{name}' already exists in '{parent}'"
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
class InvalidPath(FSError):
|
|
|
|
"The path of self is invalid, and cannot be worked with."
|
|
|
|
cls_message = "'{name}' is invalid."
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
class InvalidDestinationError(FSError):
|
|
|
|
"""A copy/move operation has been called, but the destination is invalid."""
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
cls_message = "'{name}' is an invalid destination for this operation."
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
class OperationError(FSError):
|
2014-10-13 19:08:59 +00:00
|
|
|
"""A copy/move/delete operation has been called, but the checkup after the
|
2009-10-22 15:23:32 +00:00
|
|
|
operation shows that it didn't work."""
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
cls_message = "Operation on '{name}' failed."
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2021-10-29 04:22:12 +00:00
|
|
|
class FilesDB:
|
|
|
|
|
|
|
|
create_table_query = "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, md5 BLOB, md5partial BLOB)"
|
|
|
|
drop_table_query = "DROP TABLE files;"
|
|
|
|
select_query = "SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns"
|
|
|
|
insert_query = """
|
|
|
|
INSERT INTO files (path, size, mtime_ns, entry_dt, {key}) VALUES (:path, :size, :mtime_ns, datetime('now'), :value)
|
|
|
|
ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value;
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.conn = None
|
|
|
|
self.cur = None
|
|
|
|
self.lock = None
|
|
|
|
|
|
|
|
def connect(self, path):
|
|
|
|
# type: (str, ) -> None
|
|
|
|
|
|
|
|
self.conn = sqlite3.connect(path, check_same_thread=False)
|
|
|
|
self.cur = self.conn.cursor()
|
|
|
|
self.cur.execute(self.create_table_query)
|
|
|
|
self.lock = Lock()
|
|
|
|
|
|
|
|
def clear(self):
|
|
|
|
# type: () -> None
|
|
|
|
|
|
|
|
with self.lock:
|
|
|
|
self.cur.execute(self.drop_table_query)
|
|
|
|
self.cur.execute(self.create_table_query)
|
|
|
|
|
|
|
|
def get(self, path, key):
|
|
|
|
# type: (Path, str) -> bytes
|
|
|
|
|
|
|
|
stat = path.stat()
|
|
|
|
size = stat.st_size
|
|
|
|
mtime_ns = stat.st_mtime_ns
|
|
|
|
|
|
|
|
with self.lock:
|
|
|
|
self.cur.execute(self.select_query.format(key=key), {"path": str(path), "size": size, "mtime_ns": mtime_ns})
|
|
|
|
result = self.cur.fetchone()
|
|
|
|
|
|
|
|
if result:
|
|
|
|
return result[0]
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
def put(self, path, key, value):
|
|
|
|
# type: (Path, str, Any) -> None
|
|
|
|
|
|
|
|
stat = path.stat()
|
|
|
|
size = stat.st_size
|
|
|
|
mtime_ns = stat.st_mtime_ns
|
|
|
|
|
|
|
|
with self.lock:
|
|
|
|
self.cur.execute(
|
|
|
|
self.insert_query.format(key=key),
|
|
|
|
{"path": str(path), "size": size, "mtime_ns": mtime_ns, "value": value},
|
|
|
|
)
|
|
|
|
|
|
|
|
def commit(self):
|
|
|
|
# type: () -> None
|
|
|
|
|
|
|
|
with self.lock:
|
|
|
|
self.conn.commit()
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
# type: () -> None
|
|
|
|
|
|
|
|
with self.lock:
|
|
|
|
self.cur.close()
|
|
|
|
self.conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
filesdb = FilesDB() # Singleton
|
|
|
|
|
|
|
|
|
2011-01-11 10:59:53 +00:00
|
|
|
class File:
|
2021-08-15 09:10:18 +00:00
|
|
|
"""Represents a file and holds metadata to be used for scanning."""
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2021-08-15 09:10:18 +00:00
|
|
|
INITIAL_INFO = {"size": 0, "mtime": 0, "md5": b"", "md5partial": b"", "md5samples": b""}
|
2012-05-29 21:39:54 +00:00
|
|
|
# Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of
|
|
|
|
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
|
|
|
|
# even greater when we take into account read attributes (70%!). Yeah, it's worth it.
|
2021-10-29 04:22:12 +00:00
|
|
|
__slots__ = ("path", "is_ref", "words") + tuple(INITIAL_INFO.keys())
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2021-10-29 04:22:12 +00:00
|
|
|
def __init__(self, path):
|
2009-10-22 15:23:32 +00:00
|
|
|
self.path = path
|
2012-05-29 21:39:54 +00:00
|
|
|
for attrname in self.INITIAL_INFO:
|
|
|
|
setattr(self, attrname, NOT_SET)
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2011-04-12 11:22:29 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return "<{} {}>".format(self.__class__.__name__, str(self.path))
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2012-05-29 21:39:54 +00:00
|
|
|
def __getattribute__(self, attrname):
|
|
|
|
result = object.__getattribute__(self, attrname)
|
|
|
|
if result is NOT_SET:
|
2009-10-22 15:23:32 +00:00
|
|
|
try:
|
|
|
|
self._read_info(attrname)
|
|
|
|
except Exception as e:
|
2021-08-15 09:10:18 +00:00
|
|
|
logging.warning("An error '%s' was raised while decoding '%s'", e, repr(self.path))
|
2012-05-29 21:39:54 +00:00
|
|
|
result = object.__getattribute__(self, attrname)
|
|
|
|
if result is NOT_SET:
|
|
|
|
result = self.INITIAL_INFO[attrname]
|
|
|
|
return result
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2021-10-29 04:22:12 +00:00
|
|
|
def _calc_md5(self):
|
|
|
|
# type: () -> bytes
|
|
|
|
|
|
|
|
with self.path.open("rb") as fp:
|
|
|
|
md5 = hashlib.md5()
|
|
|
|
# The goal here is to not run out of memory on really big files. However, the chunk
|
|
|
|
# size has to be large enough so that the python loop isn't too costly in terms of
|
|
|
|
# CPU.
|
|
|
|
CHUNK_SIZE = 1024 * 1024 # 1 mb
|
|
|
|
filedata = fp.read(CHUNK_SIZE)
|
|
|
|
while filedata:
|
|
|
|
md5.update(filedata)
|
|
|
|
filedata = fp.read(CHUNK_SIZE)
|
|
|
|
return md5.digest()
|
|
|
|
|
|
|
|
def _calc_md5partial(self):
|
|
|
|
# type: () -> bytes
|
|
|
|
|
|
|
|
# This offset is where we should start reading the file to get a partial md5
|
|
|
|
# For audio file, it should be where audio data starts
|
|
|
|
offset, size = (0x4000, 0x4000)
|
|
|
|
|
|
|
|
with self.path.open("rb") as fp:
|
|
|
|
fp.seek(offset)
|
|
|
|
partialdata = fp.read(size)
|
|
|
|
return hashlib.md5(partialdata).digest()
|
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
def _read_info(self, field):
|
2021-06-21 20:44:05 +00:00
|
|
|
# print(f"_read_info({field}) for {self}")
|
2020-01-01 02:16:27 +00:00
|
|
|
if field in ("size", "mtime"):
|
2012-08-09 14:53:24 +00:00
|
|
|
stats = self.path.stat()
|
2009-10-22 15:23:32 +00:00
|
|
|
self.size = nonone(stats.st_size, 0)
|
|
|
|
self.mtime = nonone(stats.st_mtime, 0)
|
2020-01-01 02:16:27 +00:00
|
|
|
elif field == "md5partial":
|
2009-10-22 15:23:32 +00:00
|
|
|
try:
|
2021-10-29 04:22:12 +00:00
|
|
|
self.md5partial = filesdb.get(self.path, "md5partial")
|
|
|
|
if self.md5partial is None:
|
|
|
|
self.md5partial = self._calc_md5partial()
|
|
|
|
filesdb.put(self.path, "md5partial", self.md5partial)
|
2020-11-27 06:49:14 +00:00
|
|
|
except Exception as e:
|
|
|
|
logging.warning("Couldn't get md5partial for %s: %s", self.path, e)
|
2020-01-01 02:16:27 +00:00
|
|
|
elif field == "md5":
|
2009-10-22 15:23:32 +00:00
|
|
|
try:
|
2021-10-29 04:22:12 +00:00
|
|
|
self.md5 = filesdb.get(self.path, "md5")
|
|
|
|
if self.md5 is None:
|
|
|
|
self.md5 = self._calc_md5()
|
|
|
|
filesdb.put(self.path, "md5", self.md5)
|
2020-11-27 06:49:14 +00:00
|
|
|
except Exception as e:
|
|
|
|
logging.warning("Couldn't get md5 for %s: %s", self.path, e)
|
2021-06-21 17:03:21 +00:00
|
|
|
elif field == "md5samples":
|
|
|
|
try:
|
|
|
|
with self.path.open("rb") as fp:
|
2021-06-21 20:44:05 +00:00
|
|
|
size = self.size
|
2021-08-13 18:38:33 +00:00
|
|
|
# Might as well hash such small files entirely.
|
2021-08-13 19:33:21 +00:00
|
|
|
if size <= MIN_FILE_SIZE:
|
2021-08-13 18:38:33 +00:00
|
|
|
setattr(self, field, self.md5)
|
|
|
|
return
|
|
|
|
|
2021-06-21 17:03:21 +00:00
|
|
|
# Chunk at 25% of the file
|
2021-06-21 20:44:05 +00:00
|
|
|
fp.seek(floor(size * 25 / 100), 0)
|
2021-06-21 17:03:21 +00:00
|
|
|
filedata = fp.read(CHUNK_SIZE)
|
2021-06-21 20:44:05 +00:00
|
|
|
md5 = hashlib.md5(filedata)
|
2021-06-21 17:03:21 +00:00
|
|
|
|
|
|
|
# Chunk at 60% of the file
|
2021-06-21 20:44:05 +00:00
|
|
|
fp.seek(floor(size * 60 / 100), 0)
|
2021-06-21 17:03:21 +00:00
|
|
|
filedata = fp.read(CHUNK_SIZE)
|
2021-06-21 20:44:05 +00:00
|
|
|
md5.update(filedata)
|
2021-06-21 17:03:21 +00:00
|
|
|
|
|
|
|
# Last chunk of the file
|
|
|
|
fp.seek(-CHUNK_SIZE, 2)
|
|
|
|
filedata = fp.read(CHUNK_SIZE)
|
2021-06-21 20:44:05 +00:00
|
|
|
md5.update(filedata)
|
|
|
|
setattr(self, field, md5.digest())
|
2021-06-21 17:03:21 +00:00
|
|
|
except Exception as e:
|
|
|
|
logging.error(f"Error computing md5samples: {e}")
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
def _read_all_info(self, attrnames=None):
|
|
|
|
"""Cache all possible info.
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
If `attrnames` is not None, caches only attrnames.
|
|
|
|
"""
|
|
|
|
if attrnames is None:
|
2012-05-29 21:39:54 +00:00
|
|
|
attrnames = self.INITIAL_INFO.keys()
|
2009-10-22 15:23:32 +00:00
|
|
|
for attrname in attrnames:
|
2012-05-29 21:39:54 +00:00
|
|
|
getattr(self, attrname)
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
# --- Public
|
2009-10-22 15:23:32 +00:00
|
|
|
@classmethod
|
|
|
|
def can_handle(cls, path):
|
2021-08-15 09:10:18 +00:00
|
|
|
"""Returns whether this file wrapper class can handle ``path``."""
|
2012-08-09 14:53:24 +00:00
|
|
|
return not path.islink() and path.isfile()
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2009-10-23 12:56:52 +00:00
|
|
|
def rename(self, newname):
|
|
|
|
if newname == self.name:
|
|
|
|
return
|
2013-11-16 17:06:16 +00:00
|
|
|
destpath = self.path.parent()[newname]
|
2012-08-09 14:53:24 +00:00
|
|
|
if destpath.exists():
|
2013-11-16 17:06:16 +00:00
|
|
|
raise AlreadyExistsError(newname, self.path.parent())
|
2009-10-22 15:23:32 +00:00
|
|
|
try:
|
2012-08-09 14:53:24 +00:00
|
|
|
self.path.rename(destpath)
|
2009-10-22 15:23:32 +00:00
|
|
|
except EnvironmentError:
|
|
|
|
raise OperationError(self)
|
2012-08-09 14:53:24 +00:00
|
|
|
if not destpath.exists():
|
2009-10-22 15:23:32 +00:00
|
|
|
raise OperationError(self)
|
|
|
|
self.path = destpath
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2013-07-14 21:43:58 +00:00
|
|
|
def get_display_info(self, group, delta):
|
2021-08-15 09:10:18 +00:00
|
|
|
"""Returns a display-ready dict of dupe's data."""
|
2013-07-14 21:43:58 +00:00
|
|
|
raise NotImplementedError()
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
# --- Properties
|
2009-10-22 15:23:32 +00:00
|
|
|
@property
|
|
|
|
def extension(self):
|
|
|
|
return get_file_ext(self.name)
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
2013-11-16 17:06:16 +00:00
|
|
|
return self.path.name
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2011-06-15 15:58:33 +00:00
|
|
|
@property
|
|
|
|
def folder_path(self):
|
2013-11-16 17:06:16 +00:00
|
|
|
return self.path.parent()
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2009-10-22 15:23:32 +00:00
|
|
|
|
2011-04-12 11:22:29 +00:00
|
|
|
class Folder(File):
|
|
|
|
"""A wrapper around a folder path.
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2021-08-13 18:38:33 +00:00
|
|
|
It has the size/md5 info of a File, but its value is the sum of its subitems.
|
2011-04-12 11:22:29 +00:00
|
|
|
"""
|
2020-01-01 02:16:27 +00:00
|
|
|
|
|
|
|
__slots__ = File.__slots__ + ("_subfolders",)
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2021-10-29 04:22:12 +00:00
|
|
|
def __init__(self, path):
|
|
|
|
File.__init__(self, path)
|
2011-04-12 11:22:29 +00:00
|
|
|
self._subfolders = None
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2011-04-12 11:22:29 +00:00
|
|
|
def _all_items(self):
|
|
|
|
folders = self.subfolders
|
2021-10-29 04:22:12 +00:00
|
|
|
files = get_files(self.path)
|
2011-04-12 11:22:29 +00:00
|
|
|
return folders + files
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2011-04-12 11:22:29 +00:00
|
|
|
def _read_info(self, field):
|
2021-06-21 20:44:05 +00:00
|
|
|
# print(f"_read_info({field}) for Folder {self}")
|
2020-01-01 02:16:27 +00:00
|
|
|
if field in {"size", "mtime"}:
|
2011-04-12 11:22:29 +00:00
|
|
|
size = sum((f.size for f in self._all_items()), 0)
|
|
|
|
self.size = size
|
2012-08-09 14:53:24 +00:00
|
|
|
stats = self.path.stat()
|
2011-04-12 11:22:29 +00:00
|
|
|
self.mtime = nonone(stats.st_mtime, 0)
|
2021-06-21 20:44:05 +00:00
|
|
|
elif field in {"md5", "md5partial", "md5samples"}:
|
2011-04-12 11:22:29 +00:00
|
|
|
# What's sensitive here is that we must make sure that subfiles'
|
|
|
|
# md5 are always added up in the same order, but we also want a
|
|
|
|
# different md5 if a file gets moved in a different subdirectory.
|
2021-06-21 17:03:21 +00:00
|
|
|
|
2011-04-12 11:22:29 +00:00
|
|
|
def get_dir_md5_concat():
|
|
|
|
items = self._all_items()
|
2014-10-13 19:08:59 +00:00
|
|
|
items.sort(key=lambda f: f.path)
|
2011-04-12 11:22:29 +00:00
|
|
|
md5s = [getattr(f, field) for f in items]
|
2020-01-01 02:16:27 +00:00
|
|
|
return b"".join(md5s)
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2011-04-12 11:22:29 +00:00
|
|
|
md5 = hashlib.md5(get_dir_md5_concat())
|
|
|
|
digest = md5.digest()
|
|
|
|
setattr(self, field, digest)
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2011-04-12 11:22:29 +00:00
|
|
|
@property
|
|
|
|
def subfolders(self):
|
|
|
|
if self._subfolders is None:
|
2021-08-15 09:10:18 +00:00
|
|
|
subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
|
2021-10-29 04:22:12 +00:00
|
|
|
self._subfolders = [self.__class__(p) for p in subfolders]
|
2011-04-12 11:22:29 +00:00
|
|
|
return self._subfolders
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2011-04-14 10:55:50 +00:00
|
|
|
@classmethod
|
|
|
|
def can_handle(cls, path):
|
2012-08-09 14:53:24 +00:00
|
|
|
return not path.islink() and path.isdir()
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2011-04-12 11:22:29 +00:00
|
|
|
|
2021-10-29 04:22:12 +00:00
|
|
|
def get_file(path, fileclasses=[File]):
|
2013-08-18 22:36:09 +00:00
|
|
|
"""Wraps ``path`` around its appropriate :class:`File` class.
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2013-08-18 22:36:09 +00:00
|
|
|
Whether a class is "appropriate" is decided by :meth:`File.can_handle`
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2013-08-18 22:36:09 +00:00
|
|
|
:param Path path: path to wrap
|
|
|
|
:param fileclasses: List of candidate :class:`File` classes
|
|
|
|
"""
|
2009-10-23 12:56:52 +00:00
|
|
|
for fileclass in fileclasses:
|
|
|
|
if fileclass.can_handle(path):
|
2021-10-29 04:22:12 +00:00
|
|
|
return fileclass(path)
|
2009-10-23 12:56:52 +00:00
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2021-10-29 04:22:12 +00:00
|
|
|
def get_files(path, fileclasses=[File]):
|
2013-08-18 22:36:09 +00:00
|
|
|
"""Returns a list of :class:`File` for each file contained in ``path``.
|
2014-10-13 19:08:59 +00:00
|
|
|
|
2013-08-18 22:36:09 +00:00
|
|
|
:param Path path: path to scan
|
|
|
|
:param fileclasses: List of candidate :class:`File` classes
|
|
|
|
"""
|
2009-10-23 12:56:52 +00:00
|
|
|
assert all(issubclass(fileclass, File) for fileclass in fileclasses)
|
2009-10-22 15:23:32 +00:00
|
|
|
try:
|
2009-10-23 12:56:52 +00:00
|
|
|
result = []
|
2013-11-16 17:06:16 +00:00
|
|
|
for path in path.listdir():
|
2021-10-29 04:22:12 +00:00
|
|
|
file = get_file(path, fileclasses=fileclasses)
|
2009-10-23 12:56:52 +00:00
|
|
|
if file is not None:
|
|
|
|
result.append(file)
|
|
|
|
return result
|
2009-10-22 15:23:32 +00:00
|
|
|
except EnvironmentError:
|
|
|
|
raise InvalidPath(path)
|