Source code for core.importer

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Import user content from an old DF or Starter Pack install.

The content to import is defined in PyLNP.json

Two import strategies are currently supported:

    copy a file or directory contents, non-recursive, no overwriting
    prepend imported file content (for logfiles)

These strategies support the 'low-hanging fruit' of imports.  Other content
or more advanced strategies have been identified, but are difficult to
implement without risking a 'bad import' scenario:

:init files:
    Not simply copyable. Sophisticated merging (similar to graphics
    upgrades) may lead to bad config when using settings from an older
    version of DF.  Will not be supported.
    Could be imported by minimising interface.txt (and ``LNP/Keybinds/*``)
    (see core/, and copying if a duplicate set is not yet
    available.  Planned for future update.
:world_gen, embark_profiles:
    Importing world gen and embark profiles may be supported eventually.
    No obvious downsides beyond tricky implementation.
    Custom settings importer - e.g. which graphics pack, are aquifers
    disabled, other PyLNP settings...  May be added later but no plans.


import os
import shutil

from . import log, paths
from .lnp import lnp

[docs]def strat_fallback(strat): """Log error if an unknown strategy is attempted.""" def __fallback(src, dest): # pylint:disable=unused-argument log.w('Attempted to use unknown strategy ' + strat) return False return __fallback
[docs]def strat_copy_add(src, dest): """Copy a file or directory contents from src to dest, without overwriting. If a single file, an existing file may be overwritten if it only contains whitespace. For directory contents, only the top level is 'filled in'. """ # handle the simple case, one file if os.path.isfile(src): if os.path.isfile(dest): with open(dest, encoding="utf-8") as f: if log.i('Skipping import of {} to {}; dest is non-empty file' .format(src, dest)) return False log.i('importing {} to {} by copying'.format(src, dest)) shutil.copy2(src, dest) return True # adding dir contents ret = False for it in os.listdir(src): if os.path.exists(os.path.join(dest, it)): log.i('Skipping import of {}/{}, exists in dest'.format(src, it)) continue ret = True # *something* was imported log.i('importing {} from {} to {}'.format(it, src, dest)) if not os.path.isdir(dest): os.makedirs(dest) item = os.path.join(src, it) if os.path.isfile(item): shutil.copy2(item, dest) else: shutil.copytree(item, os.path.join(dest, it)) return ret
[docs]def strat_text_prepend(src, dest): """Prepend the src textfile to the dest textfile, creating it if needed.""" if not os.path.isfile(src): log.i('Cannot import {} - not a file'.format(src)) return False if not os.path.isfile(dest): log.i('importing {} to {} by copying'.format(src, dest)) shutil.copy2(src, dest) return True with open(src, encoding='latin1') as f: srctext = with open(dest, encoding='latin1') as f: desttext = with open(src, 'w', encoding='latin1') as f: log.i('importing {} to {} by prepending'.format(src, dest)) f.writelines([srctext, '\n', desttext]) return True
[docs]def do_imports(from_df_dir): """Import content (defined in PyLNP.json) from the given previous df_dir, and associated LNP install if any. """ # pylint:disable=too-many-branches # validate that from_df_dir is, in fact, a DF dir if not all(os.path.exists(os.path.join(from_df_dir, *p)) for p in [('data', 'init', 'init.txt'), ('raw', 'objects')]): return (False, 'Does not seem to be a DF install directory.') # Get list of paths, and add dest where implicit (ie same as src) if not lnp.config.get('to_import'): return (False, 'Nothing is configured for import in PyLNP.json') raw_config = [(c + [c[1]])[:3] for c in lnp.config['to_import']] path_pairs = [] # Turn "paths" in PyLNP.json into real paths for st, src, dest in raw_config: if '<df>' in src: newsrc = src.replace('<df>', from_df_dir) elif '<dfhack_config>' in src: if os.path.exists(os.path.join(from_df_dir, 'hack', 'init')): newsrc = src.replace('<dfhack_config>', os.path.join( from_df_dir, 'dfhack-config', 'init')) else: newsrc = src.replace('<dfhack_config>', from_df_dir) else: newsrc = os.path.join(from_df_dir, '../', src) newsrc = os.path.abspath(os.path.normpath(newsrc)) if '<df>' in dest: newdest = dest.replace('<df>', paths.get('df')) elif '<dfhack_config>' in src: newdest = dest.replace( '<dfhack_config>', paths.get('dfhack_config')) else: newdest = paths.get('root', dest) newdest = os.path.abspath(os.path.normpath(newdest)) path_pairs.append((st, newsrc, newdest)) # Sanity-check the provided paths... src_prefix = os.path.commonprefix([src for _, src, _ in path_pairs]) dest_prefix = os.path.commonprefix([dest for _, _, dest in path_pairs]) log.i('Importing from {} to {}'.format(src_prefix, dest_prefix)) if not (os.path.isdir(src_prefix) or os.path.dirname(src_prefix)): # parent dir is a real path, even when os.path.commonprefix isn't msg = 'Can only import content from single basedir' log.w(msg) return (False, msg) if not dest_prefix: # checking <base>.startswith avoids the os.path.commonprefix issue msg = 'Can only import content to destinations below current basedir' log.w(msg) return (False, msg) strat_funcs = { 'copy_add': strat_copy_add, 'text_prepend': strat_text_prepend, } imported = [] for strat, src, dest in path_pairs: if not os.path.exists(src): log.w('Cannot import {} - does not exist'.format(src)) continue if strat_funcs.get(strat, strat_fallback(strat))(src, dest): imported.append(src) if not imported: return (False, 'Nothing was found to import!') return (True, '\n'.join(imported))