Last active
December 2, 2015 18:18
-
-
Save rgant/daa76fa15ff00a3e00ff to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
Rename local mac files to be compatible with windows shares | |
""" | |
# from __future__ import unicode_literals | |
# from builtins import * # pylint: disable=unused-wildcard-import,redefined-builtin,wildcard-import | |
import argparse | |
import logging | |
import os | |
import re | |
def windows_safe(a_name): | |
""" | |
Remaps characters not allowed in windows. | |
https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx | |
:param str a_name: A potentially non-windows safe name of a file or directory. | |
:return: Cleaned name safe for use on Windows file systems. | |
:rtype: str | |
""" | |
unicode_map = {'<': u'\ufe64', | |
'>': u'\ufe65', | |
':': u'\ufe55', | |
'"': u'\uff02', | |
'\\': u'\ufe68', | |
'/': u'\u2215', | |
'|': u'\uff5c', | |
'?': u'\ufe56', | |
'*': u'\u2731'} | |
trans_char = re.compile(r'[\x00-\x1f%s]' % ''.join([re.escape(k) for k in unicode_map.keys()])) | |
replace_unicode = lambda m: unicode_map.get(m.group(0), '_') | |
trailing_space = re.compile(r' +$') | |
clean_name = trailing_space.sub('_', a_name) | |
if clean_name[-1] == '.': | |
clean_name = clean_name[:-1] + u'\uFF0E' | |
return trans_char.sub(replace_unicode, clean_name) | |
def move_file(file_name, clean_name, root, num_processed, dryrun=False): | |
""" | |
:param str file_name: Original name of the file. | |
:param str clean_name: Windows safe name for the file. | |
:param str root: Path to the file. | |
:param int num_processed: Number of files/dirs processed so far. Used to generate | |
a unique name on conflict. | |
:param bool dryrun: Don't actually move the file, but perform the rest of the | |
operations. | |
""" | |
logger = logging.getLogger(__name__) | |
src_path = os.path.join(root, file_name) | |
dst_path = os.path.join(root, clean_name) | |
if not os.path.isfile(src_path): | |
logger.error('Could not find original file: ==>%s<==', src_path) | |
if not os.access(src_path, os.R_OK | os.W_OK): | |
logger.error('Permissions issue with file %s', src_path) | |
if os.path.exists(dst_path): | |
logger.error('Found the clean file name already: ==>%s<==', dst_path) | |
if '.' in clean_name: | |
parts = clean_name.rpartition('.') | |
parts[0] += '-%x' % num_processed | |
clean_name = ''.join(parts) | |
else: | |
clean_name += '-%x' % num_processed | |
dst_path = os.path.join(root, clean_name) | |
assert not os.path.exists(dst_path) | |
logger.info('\nRenaming: ==>%s<==\nTo File : ==>%s<==', src_path, dst_path) | |
if not dryrun: | |
try: | |
os.rename(src_path, dst_path) | |
except OSError, exc: # Python2.5 doesn't support 'as' | |
logger.error('Failed to move file: %s', exc) | |
else: | |
assert os.path.isfile(dst_path) | |
def move_dir(dir_name, clean_name, root, num_processed, dryrun=False): | |
""" | |
:param str dir_name: Original name of the directory. | |
:param str clean_name: Windows safe name for the directory. | |
:param str root: Path to the directory. | |
:param int num_processed: Number of files/dirs processed so far. Used to generate | |
a unique name on conflict. | |
:param bool dryrun: Don't actually move the directory, but perform the rest of the | |
operations. | |
""" | |
logger = logging.getLogger(__name__) | |
src_path = os.path.join(root, dir_name) | |
dst_path = os.path.join(root, clean_name) | |
if not os.path.isdir(src_path): | |
logger.error('Could not find original dir: ==>%s<==', src_path) | |
if not os.access(src_path, os.R_OK | os.W_OK | os.X_OK): | |
logger.error('Permissions issue with dir %s', src_path) | |
if os.path.exists(dst_path): | |
logger.error('Found the clean dir name already: ==>%s<==', dst_path) | |
clean_name += '-%x' % num_processed | |
dst_path = os.path.join(root, clean_name) | |
assert not os.path.exists(dst_path) | |
logger.info('\nRenaming: ==>%s<==\nTo Dir : ==>%s<==', src_path, dst_path) | |
if not dryrun: | |
try: | |
os.rename(src_path, dst_path) | |
except OSError, exc: # Python2.5 doesn't support 'as' | |
logger.error('Failed to move dir: %s', exc) | |
else: | |
assert os.path.isdir(dst_path) | |
def rename_path(src_path, dryrun=False): | |
""" | |
Walk src_path depth first. Identify and fix problems. Rename fixed files. | |
:param str src_path: Path to directory of files to make safe for windows file systems. | |
:param bool dryrun: Do not perform the actual moves, but do the other operations. | |
""" | |
num_processed = 0 | |
logger = logging.getLogger(__name__) | |
for root, dirs, files in os.walk(src_path, topdown=False): | |
if not os.access(root, os.R_OK | os.W_OK | os.X_OK): | |
logger.error('Permissions issue with root directory %s', root) | |
for file_name in files: | |
num_processed += 1 | |
# Cleanup these issues | |
clean_name = windows_safe(file_name) | |
assert clean_name | |
if file_name != clean_name: | |
move_file(file_name, clean_name, root, num_processed, dryrun) | |
else: | |
file_path = os.path.join(root, file_name) | |
logger.debug('No Changes for file: ==>%s<==', file_path) | |
for dir_name in dirs: | |
num_processed += 1 | |
# Cleanup these issues | |
clean_name = windows_safe(dir_name) | |
assert clean_name | |
if dir_name != clean_name: | |
move_dir(dir_name, clean_name, root, num_processed, dryrun) | |
else: | |
dir_path = os.path.join(root, dir_name) | |
logger.debug('No Changes for dir: ==>%s<==', dir_path) | |
def configure(): | |
""" Gather arguments from the command invocation and configure the loggers appropriately. """ | |
parser = argparse.ArgumentParser(description='Rename files to a more restrictive' + | |
' Windows compatible name.') | |
parser.add_argument('--dry-run', dest='dryrun', action='store_true', default=False, | |
help="Do not actually rename files, only output what script would do.") | |
parser.add_argument('--debug', dest='debug', action='store_true', default=False, | |
help="Change logging level to debug") | |
parser.add_argument('source_path', metavar='<path to files>', | |
help='Path to files to rename') | |
args = parser.parse_args() | |
file_formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(module)s:%(lineno)d:%(message)s') | |
file_handler = logging.FileHandler('renamer.log') | |
file_handler.setFormatter(file_formatter) | |
file_handler.setLevel(logging.INFO if not args.debug else logging.DEBUG) | |
stream_formatter = logging.Formatter('%(levelname)s:%(module)s:%(lineno)d:%(message)s') | |
stream_handler = logging.StreamHandler() | |
stream_handler.setFormatter(stream_formatter) | |
stream_handler.setLevel(logging.ERROR) | |
root_logger = logging.getLogger() | |
root_logger.addHandler(file_handler) | |
root_logger.addHandler(stream_handler) | |
# Seems like you need to have the root debugger level set to the lowest level to get the other | |
# handlers to work. Esp in older versions of python (2.5) | |
root_logger.setLevel(logging.DEBUG) | |
return args | |
def main(): | |
""" Parse arguments and initalize processor. """ | |
args = configure() | |
# Make sure we walk the files as future unicode strings. | |
rename_path(unicode(args.source_path), dryrun=args.dryrun) | |
if __name__ == "__main__": | |
try: | |
main() | |
except SystemExit: | |
raise | |
except: # pylint: disable=bare-except | |
logging.exception('Uncaught Exception') | |
raise |
Escape the keys in unicode_map so that they don't get treated special in the character class. This was an issue for the backslash, and it could also have been an issue if a dash was a key.
Add checks for permissions issues with files or directories that the script is going to rename.
Wrap the actual renames in a try except to catch OSError if the move doesn't succeed for some reason. This is to make it so we just log errors like this: "OSError: [Errno 1] Operation not permitted"
Change the except OSError as exc
to OSError, exc
to support python 2.5
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Changed around logging.