def select_success(self, path): Logger.info('Modlist: User selected the directory: {}'.format(path)) if not os.path.isdir(path): return 'Not a directory or unreadable:\n{}'.format(path) if not is_dir_writable(path): return 'Directory {} is not writable'.format(path) # Prevent idiot-loops (seriously, people doing this are idiots!) already_used_by = path_already_used_for_mod(path, self.owner.all_existing_mods) settings = kivy.app.App.get_running_app().settings if not path_can_be_a_mod(path, settings.get('launcher_moddir')) or \ (already_used_by and already_used_by != self.mod.foldername): message = textwrap.dedent(''' Really??? You're now selecting the location where {} is ALREADY installed and you've chosen: {} You realize that selecting that directory will cause EVERYTHING inside that is not part of that mod to be DELETED? Think twice next time! ''').format(self.mod.foldername, path) return message if casefold(path) == casefold(self.mod.get_full_path()): Logger.info('select_success: Selected directory is the current one. Ignoring...') return self.set_new_path(path)
def path_can_be_a_mod(path, mods_directory): """Check if a given path could be used by a mod. path - patch to be checked. mods_directory - the directory where mods are stored by the launcher. """ launcher_moddir = os.path.realpath(mods_directory) launcher_moddir_casefold = unicode_helpers.casefold(launcher_moddir) path_casefold = unicode_helpers.casefold(os.path.realpath(path)) # Loop to parent (infinite loop) if launcher_moddir_casefold == path_casefold or \ launcher_moddir_casefold.startswith(path_casefold + os.path.sep): Logger.info("path_can_be_a_mod: Rejecting {}. Loop to parent.".format( path_casefold)) return False directory_name = os.path.basename(path_casefold) if not directory_name: # Path ends with a '\' or '/' directory_name = os.path.dirname(path_casefold) # All names must be changed to lowercase bad_directories = [ 'steam', 'steamapps', 'workshop', 'content', '107410', 'common', 'arma 3', 'desktop', ] if directory_name in bad_directories: Logger.info( "path_can_be_a_mod: Rejecting {}. Blacklisted directory.".format( path_casefold)) return False if len(path_casefold) == 3 and path_casefold.endswith(':\\'): Logger.info("path_can_be_a_mod: Rejecting {}. Root directory.".format( path_casefold)) return False if path_casefold == unicode_helpers.casefold( paths.get_user_home_directory()): Logger.info("path_can_be_a_mod: Rejecting {}. Home directory.".format( path_casefold)) return False return True
def path_already_used_for_mod(path, all_existing_mods): """Check if a given path is already used by a mod and return its name. Return None otherwise. """ path = unicode_helpers.casefold(os.path.realpath(path)) for mod in all_existing_mods: mod_full_path = unicode_helpers.casefold(mod.get_full_path()) mod_real_full_path = unicode_helpers.casefold(mod.get_real_full_path()) if path == mod_full_path or \ path == mod_real_full_path or \ path.startswith(mod_full_path + os.path.sep) or \ path.startswith(mod_real_full_path + os.path.sep): return mod.foldername return None
def keep_meaningful_data(name): """Return the name after it has been changed to lowercase and stripped of all letters that are not latin characters or digits or '@'. This is done for a pseudo-fuzzy comparison where "@Kunduz, Afghanistan" and "@Kunduz Afghanistan" will match. """ no_case = casefold(name) allowed_chars = string.letters + string.digits + '@' filtered_name = re.sub('[^{}]'.format(re.escape(allowed_chars)), '', no_case) return filtered_name
def check_mod_directories(files_list, base_directory, check_subdir='', on_superfluous='warn', checksums=None, case_sensitive=False): """Check if all files and directories present in the mod directories belong to the torrent file. If not, remove those if on_superfluous=='remove' or return False if on_superfluous=='warn'. base_directory is the directory to which mods are downloaded. For example: if the mod directory is C:\Arma\@MyMod, base_directory should be C:\Arma. check_subdir tells the function to only check if files contained in the subdirectory are properly created and existing. on_superfluous is the action to perform when superfluous files are found: 'warn': return False 'remove': remove the file or directory 'ignore': do nothing To prevent accidental file removal, this function will only remove files that are at least one directory deep in the file structure! As all multi-file torrents *require* one root directory that holds those files, this should not be an issue. This function will skip files or directories that match the 'WHITELIST_NAME' variable. If the dictionary checksums is not None, the files' checksums will be checked. Returns if the directory has been cleaned sucessfully or if all files present are supposed to be there. Do not ignore this value! If unsuccessful at removing files, the mod should NOT be considered ready to play.""" if on_superfluous not in ('warn', 'remove', 'ignore'): raise Exception('Unknown action: {}'.format(on_superfluous)) top_dirs, dirs, file_paths, checksums = parse_files_list(files_list, checksums, check_subdir) # Remove whitelisted items from the lists dirs = filter_out_whitelisted(dirs) file_paths = filter_out_whitelisted(file_paths) # If not case sensitive, rewrite data so it may be used in a case insensitive # comparisons if not case_sensitive: file_paths = set(casefold(filename) for filename in file_paths) dirs = set(casefold(directory) for directory in dirs) top_dirs = set(casefold(top_dir) for top_dir in top_dirs) if checksums: checksums = {casefold(key): value for (key, value) in checksums.iteritems()} # Set conditional casefold function ccf = lambda x: casefold(x) else: ccf = lambda x: x base_directory = os.path.realpath(base_directory) Logger.debug('check_mod_directories: Verifying base_directory: {}'.format(base_directory)) success = True try: for directory_nocase in top_dirs: with ignore_exceptions(KeyError): dirs.remove(directory_nocase) if directory_nocase in WHITELIST_NAME: continue full_base_path = os.path.join(base_directory, directory_nocase) _unlink_safety_assert(base_directory, full_base_path, action='enter') # FIXME: on OSError, this might indicate a broken junction or symlink on windows # Must act accordingly then. for (dirpath, dirnames, filenames) in walker.walk(full_base_path, topdown=True, onerror=_raiser, followlinks=True): relative_path = os.path.relpath(dirpath, base_directory) Logger.debug('check_mod_directories: In directory: {}'.format(relative_path)) # First check files in this directory for file_name in filenames: relative_file_name_nocase = ccf(os.path.join(relative_path, file_name)) if file_name in WHITELIST_NAME: Logger.debug('check_mod_directories: File {} in WHITELIST_NAME, skipping...'.format(file_name)) with ignore_exceptions(KeyError): file_paths.remove(relative_file_name_nocase) continue full_file_path = os.path.join(dirpath, file_name) Logger.debug('check_mod_directories: Checking file: {}'.format(relative_file_name_nocase)) if relative_file_name_nocase in file_paths: file_paths.remove(relative_file_name_nocase) Logger.debug('check_mod_directories: {} present in torrent metadata'.format(relative_file_name_nocase)) if checksums and sha1(full_file_path) != checksums[relative_file_name_nocase]: Logger.debug('check_mod_directories: File {} exists but its hash differs from expected.'.format(relative_file_name_nocase)) Logger.debug('check_mod_directories: Expected: {}, computed: {}'.format(checksums[relative_file_name_nocase].encode('hex'), sha1(full_file_path).encode('hex'))) return False continue # File present in the torrent, nothing to see here if on_superfluous == 'remove': Logger.debug('check_mod_directories: Removing file: {}'.format(full_file_path)) _safer_unlink(full_base_path, full_file_path) elif on_superfluous == 'warn': Logger.debug('check_mod_directories: Superfluous file: {}'.format(full_file_path)) return False elif on_superfluous == 'ignore': pass # Now check directories # Iterate over a copy because we'll be deleting items from the original for dir_name in dirnames[:]: relative_dir_path = ccf(os.path.join(relative_path, dir_name)) if dir_name in WHITELIST_NAME: dirnames.remove(dir_name) with ignore_exceptions(KeyError): dirs.remove(relative_dir_path) continue Logger.debug('check_mod_directories: Checking dir: {}'.format(relative_dir_path)) if relative_dir_path in dirs: dirs.remove(relative_dir_path) continue # Directory present in the torrent, nothing to see here full_directory_path = os.path.join(dirpath, dir_name) if on_superfluous == 'remove': Logger.debug('check_mod_directories: Removing directory: {}'.format(full_directory_path)) dirnames.remove(dir_name) _safer_rmtree(full_base_path, full_directory_path) elif on_superfluous == 'warn': Logger.debug('check_mod_directories: Superfluous directory: {}'.format(full_directory_path)) return False elif on_superfluous == 'ignore': pass # Check for files missing on disk # file_paths contains all missing files OR files outside of any directory. # Such files will not exist with regular torrents but may happen if using # check_subdir != ''. # We just check if they exist. No deleting! for file_entry_nocase in file_paths: full_path = os.path.join(base_directory, file_entry_nocase) if not os.path.isfile(full_path): Logger.debug('check_mod_directories: File paths missing on disk, setting retval to False') Logger.debug('check_mod_directories: ' + full_path) success = False break if checksums and sha1(full_path) != checksums[file_entry_nocase]: Logger.debug('check_mod_directories: File {} exists but its hash differs from expected.'.format(file_entry_nocase)) Logger.debug('check_mod_directories: Expected: {}, computed: {}'.format(checksums[file_entry_nocase].encode('hex'), sha1(full_path).encode('hex'))) success = False break if dirs: Logger.debug('check_mod_directories: Dirs missing on disk, setting retval to False') Logger.debug('check_mod_directories: ' + ', '.join(dirs)) success = False except OSError: success = False return success