Skip to content

Instantly share code, notes, and snippets.

@rgant
Last active December 2, 2015 18:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rgant/daa76fa15ff00a3e00ff to your computer and use it in GitHub Desktop.
Save rgant/daa76fa15ff00a3e00ff to your computer and use it in GitHub Desktop.
#!/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
@rgant
Copy link
Author

rgant commented Nov 17, 2015

Added a --debug flag to arguments so you can log every file and dir that is processed.

@rgant
Copy link
Author

rgant commented Nov 17, 2015

Changed around logging.

@rgant
Copy link
Author

rgant commented Nov 18, 2015

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.

@rgant
Copy link
Author

rgant commented Nov 19, 2015

Add checks for permissions issues with files or directories that the script is going to rename.

@rgant
Copy link
Author

rgant commented Nov 20, 2015

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"

@rgant
Copy link
Author

rgant commented Nov 24, 2015

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