diff --git a/filesystem.py b/filesystem.py new file mode 100644 index 0000000..b1c341e --- /dev/null +++ b/filesystem.py @@ -0,0 +1,295 @@ +""" +General file system methods. + +.. moduleauthor:: Jaisen Mathai +""" +from __future__ import print_function +from builtins import object + +import os +import re +import shutil +import time + +from elodie import geolocation +from elodie import constants +from elodie.localstorage import Db +from elodie.media.media import Media +from elodie.media.text import Text +from elodie.media.audio import Audio +from elodie.media.photo import Photo +from elodie.media.video import Video + + +class FileSystem(object): + + """A class for interacting with the file system.""" + + def create_directory(self, directory_path): + """Create a directory if it does not already exist. + + :param str directory_name: A fully qualified path of the + to create. + :returns: bool + """ + try: + if os.path.exists(directory_path): + return True + else: + os.makedirs(directory_path) + return True + except OSError: + # OSError is thrown for cases like no permission + pass + + return False + + def delete_directory_if_empty(self, directory_path): + """Delete a directory only if it's empty. + + Instead of checking first using `len([name for name in + os.listdir(directory_path)]) == 0`, we catch the OSError exception. + + :param str directory_name: A fully qualified path of the directory + to delete. + """ + try: + os.rmdir(directory_path) + return True + except OSError: + pass + + return False + + def get_all_files(self, path, extensions=None): + """Recursively get all files which match a path and extension. + + :param str path string: Path to start recursive file listing + :param tuple(str) extensions: File extensions to include (whitelist) + """ + files = [] + for dirname, dirnames, filenames in os.walk(path): + # print path to all filenames. + for filename in filenames: + if( + extensions is None or + filename.lower().endswith(extensions) + ): + files.append(os.path.join(dirname, filename)) + return files + + def get_current_directory(self): + """Get the current working directory. + + :returns: str + """ + return os.getcwd() + + def get_file_name(self, media): + """Generate file name for a photo or video using its metadata. + + We use an ISO8601-like format for the file name prefix. Instead of + colons as the separator for hours, minutes and seconds we use a hyphen. + https://en.wikipedia.org/wiki/ISO_8601#General_principles + + :param media: A Photo or Video instance + :type media: :class:`~elodie.media.photo.Photo` or + :class:`~elodie.media.video.Video` + :returns: str or None for non-photo or non-videos + """ + if(not media.is_valid()): + return None + + metadata = media.get_metadata() + if(metadata is None): + return None + + # If the file has EXIF title we use that in the file name + # (i.e. my-favorite-photo-img_1234.jpg) + # We want to remove the date prefix we add to the name. + # This helps when re-running the program on file which were already + # processed. + base_name = re.sub( + '^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-', + '', + metadata['base_name'] + ) + if(len(base_name) == 0): + base_name = metadata['base_name'] + + if( + 'title' in metadata and + metadata['title'] is not None and + len(metadata['title']) > 0 + ): + title_sanitized = re.sub('\W+', '-', metadata['title'].strip()) + base_name = base_name.replace('-%s' % title_sanitized, '') + base_name = '%s-%s' % (base_name, title_sanitized) + + file_name = '%s-%s.%s' % ( + time.strftime( + '%Y-%m-%d_%H-%M-%S', + metadata['date_taken'] + ), + base_name, + metadata['extension']) + return file_name.lower() + + def get_folder_name_by_date(self, time_obj): + """Get date based folder name. + + :param time time_obj: Time object to be used to determine folder name. + :returns: str + """ + return time.strftime('%Y-%m-%b', time_obj) + + def get_folder_path(self, metadata): + """Get folder path by various parameters. + + :param time time_obj: Time object to be used to determine folder name. + :returns: str + """ + path = [] + if(metadata['date_taken'] is not None): + path.append(time.strftime('%Y-%m-%b', metadata['date_taken'])) + + if(metadata['album'] is not None): + path.append(metadata['album']) + elif( + metadata['latitude'] is not None and + metadata['longitude'] is not None + ): + place_name = geolocation.place_name( + metadata['latitude'], + metadata['longitude'] + ) + if(place_name is not None): + path.append(place_name) + + # if we don't have a 2nd level directory we use 'Unknown Location' + if(len(path) < 2): + path.append('Unknown Location') + + # return '/'.join(path[::-1]) + return os.path.join(*path) + + def process_file(self, _file, destination, media, **kwargs): + move = False + if('move' in kwargs): + move = kwargs['move'] + + allow_duplicate = False + if('allowDuplicate' in kwargs): + allow_duplicate = kwargs['allowDuplicate'] + + if(not media.is_valid()): + print('%s is not a valid media file. Skipping...' % _file) + return + + metadata = media.get_metadata() + + directory_name = self.get_folder_path(metadata) + + dest_directory = os.path.join(destination, directory_name) + file_name = self.get_file_name(media) + dest_path = os.path.join(dest_directory, file_name) + + db = Db() + checksum = db.checksum(_file) + if(checksum is None): + if(constants.debug is True): + print('Could not get checksum for %s. Skipping...' % _file) + return + + # If duplicates are not allowed and this hash exists in the db then we + # return + if(allow_duplicate is False and db.check_hash(checksum) is True): + if(constants.debug is True): + print('%s already exists at %s. Skipping...' % ( + _file, + db.get_hash(checksum) + )) + return + + self.create_directory(dest_directory) + + if(move is True): + stat = os.stat(_file) + shutil.move(_file, dest_path) + os.utime(dest_path, (stat.st_atime, stat.st_mtime)) + else: + shutil.copy(_file, dest_path) + self.set_date_from_filename(dest_path) + + db.add_hash(checksum, dest_path) + db.update_hash_db() + + return dest_path + + def set_date_from_filename(self, file): + """ Set the modification time on the file base on the file name. + """ + + date_taken = None + file_name = os.path.basename(file) + # Initialize date taken to what's returned from the metadata function. + # If the folder and file name follow a time format of + # YYYY-MM/DD-IMG_0001.JPG then we override the date_taken + (year, month, day) = [None] * 3 + year_month_day_match = re.search('(\d{4})-(\d{2})-(\d{2})', file_name) + if(year_month_day_match is not None): + (year, month, day) = year_month_day_match.groups() + + # check if the file system path indicated a date and if so we + # override the metadata value + if(year is not None and month is not None and day is not None): + date_taken = time.strptime( + '{}-{}-{}'.format(year, month, day), + '%Y-%m-%d' + ) + + os.utime(file, (time.time(), time.mktime(date_taken))) + + def set_date_from_path_video(self, video): + """Set the modification time on the file based on the file path. + + Noop if the path doesn't match the format YYYY-MM/DD-IMG_0001.JPG. + + :param elodie.media.video.Video video: An instance of Video. + """ + date_taken = None + + video_file_path = video.get_file_path() + + # Initialize date taken to what's returned from the metadata function. + # If the folder and file name follow a time format of + # YYYY-MM/DD-IMG_0001.JPG then we override the date_taken + (year, month, day) = [None] * 3 + directory = os.path.dirname(video_file_path) + # If the directory matches we get back a match with + # groups() = (year, month) + year_month_match = re.search('(\d{4})-(\d{2})', directory) + if(year_month_match is not None): + (year, month) = year_month_match.groups() + day_match = re.search( + '^(\d{2})', + os.path.basename(video.get_file_path()) + ) + if(day_match is not None): + day = day_match.group(1) + + # check if the file system path indicated a date and if so we + # override the metadata value + if(year is not None and month is not None): + if(day is not None): + date_taken = time.strptime( + '{}-{}-{}'.format(year, month, day), + '%Y-%m-%d' + ) + else: + date_taken = time.strptime( + '{}-{}'.format(year, month), + '%Y-%m' + ) + + os.utime(video_file_path, (time.time(), time.mktime(date_taken)))