pdlzbs/filebrowser/base.py

557 lines
17 KiB
Python

# coding: utf-8
import datetime
import mimetypes
import os
import platform
import tempfile
import time
from django.core.files import File
from django.utils.encoding import force_str
from django.utils.functional import cached_property
from six import python_2_unicode_compatible, string_types
from filebrowser.settings import EXTENSIONS, VERSIONS, ADMIN_VERSIONS, VERSIONS_BASEDIR, VERSION_QUALITY, STRICT_PIL, IMAGE_MAXBLOCK, DEFAULT_PERMISSIONS
from filebrowser.utils import path_strip, process_image
from .namers import get_namer
if STRICT_PIL:
from PIL import Image
from PIL import ImageFile
else:
try:
from PIL import Image
from PIL import ImageFile
except ImportError:
import Image
import ImageFile
from .compat import get_modified_time
ImageFile.MAXBLOCK = IMAGE_MAXBLOCK # default is 64k
class FileListing():
"""
The FileListing represents a group of FileObjects/FileDirObjects.
An example::
from filebrowser.base import FileListing
filelisting = FileListing(path, sorting_by='date', sorting_order='desc')
print filelisting.files_listing_total()
print filelisting.results_listing_total()
for fileobject in filelisting.files_listing_total():
print fileobject.filetype
where path is a relative path to a storage location
"""
# Four variables to store the length of a listing obtained by various listing methods
# (updated whenever a particular listing method is called).
_results_listing_total = None
_results_walk_total = None
_results_listing_filtered = None
_results_walk_total = None
def __init__(self, path, filter_func=None, sorting_by=None, sorting_order=None, site=None):
self.path = path
self.filter_func = filter_func
self.sorting_by = sorting_by
self.sorting_order = sorting_order
if not site:
from filebrowser.sites import site as default_site
site = default_site
self.site = site
# HELPER METHODS
# sort_by_attr
def sort_by_attr(self, seq, attr):
"""
Sort the sequence of objects by object's attribute
Arguments:
seq - the list or any sequence (including immutable one) of objects to sort.
attr - the name of attribute to sort by
Returns:
the sorted list of objects.
"""
from operator import attrgetter
if isinstance(attr, string_types): # Backward compatibility hack
attr = (attr, )
return sorted(seq, key=attrgetter(*attr))
@cached_property
def is_folder(self):
return self.site.storage.isdir(self.path)
def listing(self):
"List all files for path"
if self.is_folder:
dirs, files = self.site.storage.listdir(self.path)
return (f for f in dirs + files)
return []
def _walk(self, path, filelisting):
"""
Recursively walks the path and collects all files and
directories.
Danger: Symbolic links can create cycles and this function
ends up in a regression.
"""
dirs, files = self.site.storage.listdir(path)
if dirs:
for d in dirs:
self._walk(os.path.join(path, d), filelisting)
filelisting.extend([path_strip(os.path.join(path, d), self.site.directory)])
if files:
for f in files:
filelisting.extend([path_strip(os.path.join(path, f), self.site.directory)])
def walk(self):
"Walk all files for path"
filelisting = []
if self.is_folder:
self._walk(self.path, filelisting)
return filelisting
# Cached results of files_listing_total (without any filters and sorting applied)
_fileobjects_total = None
def files_listing_total(self):
"Returns FileObjects for all files in listing"
if self._fileobjects_total is None:
self._fileobjects_total = []
for item in self.listing():
fileobject = FileObject(os.path.join(self.path, item), site=self.site)
self._fileobjects_total.append(fileobject)
files = self._fileobjects_total
if self.sorting_by:
files = self.sort_by_attr(files, self.sorting_by)
if self.sorting_order == "desc":
files.reverse()
self._results_listing_total = len(files)
return files
def files_walk_total(self):
"Returns FileObjects for all files in walk"
files = []
for item in self.walk():
fileobject = FileObject(os.path.join(self.site.directory, item), site=self.site)
files.append(fileobject)
if self.sorting_by:
files = self.sort_by_attr(files, self.sorting_by)
if self.sorting_order == "desc":
files.reverse()
self._results_walk_total = len(files)
return files
def files_listing_filtered(self):
"Returns FileObjects for filtered files in listing"
if self.filter_func:
listing = list(filter(self.filter_func, self.files_listing_total()))
else:
listing = self.files_listing_total()
self._results_listing_filtered = len(listing)
return listing
def files_walk_filtered(self):
"Returns FileObjects for filtered files in walk"
if self.filter_func:
listing = list(filter(self.filter_func, self.files_walk_total()))
else:
listing = self.files_walk_total()
self._results_walk_filtered = len(listing)
return listing
def results_listing_total(self):
"Counter: all files"
if self._results_listing_total is not None:
return self._results_listing_total
return len(self.files_listing_total())
def results_walk_total(self):
"Counter: all files"
if self._results_walk_total is not None:
return self._results_walk_total
return len(self.files_walk_total())
def results_listing_filtered(self):
"Counter: filtered files"
if self._results_listing_filtered is not None:
return self._results_listing_filtered
return len(self.files_listing_filtered())
def results_walk_filtered(self):
"Counter: filtered files"
if self._results_walk_filtered is not None:
return self._results_walk_filtered
return len(self.files_walk_filtered())
@python_2_unicode_compatible
class FileObject():
"""
The FileObject represents a file (or directory) on the server.
An example::
from filebrowser.base import FileObject
fileobject = FileObject(path)
where path is a relative path to a storage location
"""
def __init__(self, path, site=None):
if not site:
from filebrowser.sites import site as default_site
site = default_site
self.site = site
if platform.system() == 'Windows':
self.path = path.replace('\\', '/')
else:
self.path = path
self.head = os.path.dirname(path)
self.filename = os.path.basename(path)
self.filename_lower = self.filename.lower()
self.filename_root, self.extension = os.path.splitext(self.filename)
self.mimetype = mimetypes.guess_type(self.filename)
def __str__(self):
return force_str(self.path)
def __fspath__(self):
return self.__str__()
@property
def name(self):
return self.path
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self or "None")
def __len__(self):
return len(self.path)
# HELPER METHODS
# _get_file_type
def _get_file_type(self):
"Get file type as defined in EXTENSIONS."
file_type = ''
for k, v in EXTENSIONS.items():
for extension in v:
if self.extension.lower() == extension.lower():
file_type = k
return file_type
# GENERAL ATTRIBUTES/PROPERTIES
# filetype
# filesize
# date
# datetime
# exists
@cached_property
def filetype(self):
"Filetype as defined with EXTENSIONS"
return 'Folder' if self.is_folder else self._get_file_type()
@cached_property
def filesize(self):
"Filesize in bytes"
return self.site.storage.size(self.path) if self.exists else None
@cached_property
def date(self):
"Modified time (from site.storage) as float (mktime)"
if self.exists:
return time.mktime(get_modified_time(self.site.storage, self.path).timetuple())
return None
@property
def datetime(self):
"Modified time (from site.storage) as datetime"
if self.date:
return datetime.datetime.fromtimestamp(self.date)
return None
@cached_property
def exists(self):
"True, if the path exists, False otherwise"
return self.site.storage.exists(self.path)
# PATH/URL ATTRIBUTES/PROPERTIES
# path (see init)
# path_relative_directory
# path_full
# dirname
# url
@property
def path_relative_directory(self):
"Path relative to site.directory"
return path_strip(self.path, self.site.directory)
@property
def path_full(self):
"Absolute path as defined with site.storage"
return self.site.storage.path(self.path)
@property
def dirname(self):
"The directory (not including site.directory)"
return os.path.dirname(self.path_relative_directory)
@property
def url(self):
"URL for the file/folder as defined with site.storage"
return self.site.storage.url(self.path)
# IMAGE ATTRIBUTES/PROPERTIES
# dimensions
# width
# height
# aspectratio
# orientation
@cached_property
def dimensions(self):
"Image dimensions as a tuple"
if self.filetype != 'Image':
return None
try:
im = Image.open(self.site.storage.open(self.path))
return im.size
except:
pass
@property
def width(self):
"Image width in px"
if self.dimensions:
return self.dimensions[0]
return None
@property
def height(self):
"Image height in px"
if self.dimensions:
return self.dimensions[1]
return None
@property
def aspectratio(self):
"Aspect ratio (float format)"
if self.dimensions:
return float(self.width) / float(self.height)
return None
@property
def orientation(self):
"Image orientation, either 'Landscape' or 'Portrait'"
if self.dimensions:
if self.dimensions[0] >= self.dimensions[1]:
return "Landscape"
else:
return "Portrait"
return None
# FOLDER ATTRIBUTES/PROPERTIES
# is_folder
# is_empty
@cached_property
def is_folder(self):
"True, if path is a folder"
return self.site.storage.isdir(self.path)
@property
def is_empty(self):
"True, if folder is empty. False otherwise, or if the object is not a folder."
if self.is_folder:
dirs, files = self.site.storage.listdir(self.path)
if not dirs and not files:
return True
return False
# VERSION ATTRIBUTES/PROPERTIES
# is_version
# versions_basedir
# original
# original_filename
@property
def is_version(self):
"True if file is a version, false otherwise"
return self.head.startswith(VERSIONS_BASEDIR)
@property
def versions_basedir(self):
"Main directory for storing versions (either VERSIONS_BASEDIR or site.directory)"
if VERSIONS_BASEDIR:
return VERSIONS_BASEDIR
elif self.site.directory:
return self.site.directory
else:
return ""
@property
def original(self):
"Returns the original FileObject"
if self.is_version:
relative_path = self.head.replace(self.versions_basedir, "").lstrip("/")
return FileObject(os.path.join(self.site.directory, relative_path, self.original_filename), site=self.site)
return self
@property
def original_filename(self):
"Get the filename of an original image from a version"
if not self.is_version:
return self.filename
return get_namer(
file_object=self,
filename_root=self.filename_root,
extension=self.extension,
).get_original_name()
# VERSION METHODS
# versions()
# admin_versions()
# version_name(suffix)
# version_path(suffix)
# version_generate(suffix)
def _get_options(self, version_suffix, extra_options=None):
options = dict(VERSIONS.get(version_suffix, {}))
if extra_options:
options.update(extra_options)
if 'size' in options and 'width' not in options:
width, height = options['size']
options['width'] = width
options['height'] = height
return options
def versions(self):
"List of versions (not checking if they actually exist)"
version_list = []
if self.filetype == "Image" and not self.is_version:
for version in sorted(VERSIONS):
version_list.append(os.path.join(self.versions_basedir, self.dirname, self.version_name(version)))
return version_list
def admin_versions(self):
"List of admin versions (not checking if they actually exist)"
version_list = []
if self.filetype == "Image" and not self.is_version:
for version in ADMIN_VERSIONS:
version_list.append(os.path.join(self.versions_basedir, self.dirname, self.version_name(version)))
return version_list
def version_name(self, version_suffix, extra_options=None):
"Name of a version" # FIXME: version_name for version?
options = self._get_options(version_suffix, extra_options)
return get_namer(
file_object=self,
version_suffix=version_suffix,
filename_root=self.filename_root,
extension=self.extension,
options=options,
).get_version_name()
def version_path(self, version_suffix, extra_options=None):
"Path to a version (relative to storage location)" # FIXME: version_path for version?
return os.path.join(
self.versions_basedir,
self.dirname,
self.version_name(version_suffix, extra_options))
def version_generate(self, version_suffix, extra_options=None):
"Generate a version" # FIXME: version_generate for version?
path = self.path
options = self._get_options(version_suffix, extra_options)
version_path = self.version_path(version_suffix, extra_options)
if not self.site.storage.isfile(version_path):
version_path = self._generate_version(version_path, options)
elif get_modified_time(self.site.storage, path) > get_modified_time(self.site.storage, version_path):
version_path = self._generate_version(version_path, options)
return FileObject(version_path, site=self.site)
def _generate_version(self, version_path, options):
"""
Generate Version for an Image.
value has to be a path relative to the storage location.
"""
tmpfile = File(tempfile.NamedTemporaryFile())
try:
f = self.site.storage.open(self.path)
except IOError:
return ""
im = Image.open(f)
version_dir, version_basename = os.path.split(version_path)
root, ext = os.path.splitext(version_basename)
version = process_image(im, options)
if not version:
version = im
if 'methods' in options:
for m in options['methods']:
if callable(m):
version = m(version)
# IF need Convert RGB
if ext in [".jpg", ".jpeg"] and version.mode not in ("L", "RGB"):
version = version.convert("RGB")
# save version
try:
version.save(tmpfile, format=Image.EXTENSION[ext.lower()], quality=VERSION_QUALITY, optimize=(os.path.splitext(version_path)[1] != '.gif'))
except IOError:
version.save(tmpfile, format=Image.EXTENSION[ext.lower()], quality=VERSION_QUALITY)
# remove old version, if any
if self.site.storage.path(version_path) != self.site.storage.get_available_name(version_path):
self.site.storage.delete(version_path)
self.site.storage.save(version_path, tmpfile)
# set permissions
if DEFAULT_PERMISSIONS is not None:
os.chmod(self.site.storage.path(version_path), DEFAULT_PERMISSIONS)
return version_path
# DELETE METHODS
# delete()
# delete_versions()
# delete_admin_versions()
def delete(self):
"Delete FileObject (deletes a folder recursively)"
if self.is_folder:
self.site.storage.rmtree(self.path)
else:
self.site.storage.delete(self.path)
def delete_versions(self):
"Delete versions"
for version in self.versions():
try:
self.site.storage.delete(version)
except:
pass
def delete_admin_versions(self):
"Delete admin versions"
for version in self.admin_versions():
try:
self.site.storage.delete(version)
except:
pass