#!/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_add:
copy a file or directory contents, non-recursive, no overwriting
:text_prepend:
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.
:keybinds:
Could be imported by minimising interface.txt (and ``LNP/Keybinds/*``)
(see core/keybinds.py), 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.
:other:
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 f.read().strip():
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 = f.read()
with open(dest, encoding='latin1') as f:
desttext = f.read()
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))