diff options
author | Ondrej Grover <ondrej.grover@gmail.com> | 2014-05-09 11:42:12 +0200 |
---|---|---|
committer | Ondrej Grover <ondrej.grover@gmail.com> | 2014-05-09 12:26:42 +0200 |
commit | 25a8ab1a5f12377865af19b89496b3449cb0076f (patch) | |
tree | 7efd27fa73b2a8d0c90ddb4e1ac81fee4c595bb4 | |
parent | cd40105c40285d0faec7c63200df86bf4d609cc7 (diff) | |
download | pelican-static_symlink_1042.tar.gz |
Fix #1042 enable (sym)linking of static content and sourcesstatic_symlink_1042
This can greatly speed up generation for people with lots of static
files and/or sources output.
-rw-r--r-- | docs/settings.rst | 5 | ||||
-rw-r--r-- | pelican/generators.py | 16 | ||||
-rw-r--r-- | pelican/settings.py | 2 | ||||
-rw-r--r-- | pelican/tests/test_pelican.py | 33 | ||||
-rw-r--r-- | pelican/utils.py | 37 |
5 files changed, 85 insertions, 8 deletions
diff --git a/docs/settings.rst b/docs/settings.rst index 2782977c..fba47c39 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -119,6 +119,7 @@ Setting name (followed by default value, if any) ``OUTPUT_SOURCES_EXTENSION = '.text'`` Controls the extension that will be used by the SourcesGenerator. Defaults to ``.text``. If not a valid string the default value will be used. +``OUTPUT_SOURCES_JUST_LINK = ''`` Works like ``STATIC_JUST_LINK``. ``RELATIVE_URLS = False`` Defines whether Pelican should use document-relative URLs or not. Only set this to ``True`` when developing/testing and only if you fully understand the effect it can have on links/feeds. @@ -135,6 +136,10 @@ Setting name (followed by default value, if any) on the output path "static". By default, Pelican will copy the "images" folder to the output folder. +``STATIC_JUST_LINK = ''`` Instead of copying the static files to the output directory, they can + be linked as symbolic links if set to ``symbolic`` (``rsync`` based uploads may require the ``--copy-links`` option) or as hard links if + set to ``hard``. Note that this functionality may be available only on + some operating systems and Python distributions. ``TIMEZONE`` The timezone used in the date information, to generate Atom and RSS feeds. See the *Timezone* section below for more info. diff --git a/pelican/generators.py b/pelican/generators.py index 7c6ba66b..89b88d95 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -650,13 +650,16 @@ class StaticGenerator(Generator): def _copy_paths(self, paths, source, destination, output_path, final_path=None): """Copy all the paths from source to destination""" + just_link = self.settings['STATIC_JUST_LINK'] for path in paths: if final_path: copy(os.path.join(source, path), - os.path.join(output_path, destination, final_path)) + os.path.join(output_path, destination, final_path), + just_link) else: copy(os.path.join(source, path), - os.path.join(output_path, destination, path)) + os.path.join(output_path, destination, path), + just_link) def generate_context(self): self.staticfiles = [] @@ -680,14 +683,14 @@ class StaticGenerator(Generator): def generate_output(self, writer): self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme, self.settings['THEME_STATIC_DIR'], self.output_path, - os.curdir) + os.curdir) # copy all Static files + just_link = self.settings['STATIC_JUST_LINK'] for sc in self.context['staticfiles']: source_path = os.path.join(self.path, sc.source_path) save_as = os.path.join(self.output_path, sc.save_as) mkdir_p(os.path.dirname(save_as)) - shutil.copy2(source_path, save_as) - logger.info('copying {} to {}'.format(sc.source_path, sc.save_as)) + copy(source_path, save_as, just_link) class SourceFileGenerator(Generator): @@ -699,7 +702,8 @@ class SourceFileGenerator(Generator): output_path, _ = os.path.splitext(obj.save_as) dest = os.path.join(self.output_path, output_path + self.output_extension) - copy(obj.source_path, dest) + just_link = self.settings['SOURCES_JUST_LINK'] + copy(obj.source_path, dest, just_link) def generate_output(self, writer=None): logger.info(' Generating source files...') diff --git a/pelican/settings.py b/pelican/settings.py index f759ff9e..898f9cbe 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -39,6 +39,7 @@ DEFAULT_CONFIG = { 'STATIC_PATHS': ['images', ], 'THEME_STATIC_DIR': 'theme', 'THEME_STATIC_PATHS': ['static', ], + 'STATIC_JUST_LINK': '', 'FEED_ALL_ATOM': os.path.join('feeds', 'all.atom.xml'), 'CATEGORY_FEED_ATOM': os.path.join('feeds', '%s.atom.xml'), 'AUTHOR_FEED_ATOM': os.path.join('feeds', '%s.atom.xml'), @@ -51,6 +52,7 @@ DEFAULT_CONFIG = { 'DISPLAY_CATEGORIES_ON_MENU': True, 'OUTPUT_SOURCES': False, 'OUTPUT_SOURCES_EXTENSION': '.text', + 'SOURCES_JUST_LINK': '', 'USE_FOLDER_AS_CATEGORY': True, 'DEFAULT_CATEGORY': 'misc', 'WITH_FUTURE_DATES': True, diff --git a/pelican/tests/test_pelican.py b/pelican/tests/test_pelican.py index 411fb7da..867cd13d 100644 --- a/pelican/tests/test_pelican.py +++ b/pelican/tests/test_pelican.py @@ -102,6 +102,39 @@ class TestPelican(LoggedTestCase): mute(True)(pelican.run)() self.assertDirsEqual(self.temp_path, os.path.join(OUTPUT_PATH, 'custom')) + def test_static_symlinking(self): + '''Test that symbolic linking of static files works''' + settings = read_settings(path=SAMPLE_CONFIG, override={ + 'PATH': INPUT_PATH, + 'OUTPUT_PATH': self.temp_path, + 'CACHE_PATH': self.temp_cache, + 'LOCALE': locale.normalize('en_US'), + 'STATIC_JUST_LINK': 'symbolic', + }) + pelican = Pelican(settings=settings) + mute(True)(pelican.run)() + + for fname in ['pictures/Fat_Cat.jpg', 'pictures/Sushi_Macro.jpg', 'robots.txt']: + dest = os.path.join(self.temp_path, fname) + self.assertTrue(os.path.exists(dest) and os.path.islink(dest)) + + def test_static_hardlinking(self): + '''Test that hard linking of static files works''' + settings = read_settings(path=SAMPLE_CONFIG, override={ + 'PATH': INPUT_PATH, + 'OUTPUT_PATH': self.temp_path, + 'CACHE_PATH': self.temp_cache, + 'LOCALE': locale.normalize('en_US'), + 'STATIC_JUST_LINK': 'hard', + }) + pelican = Pelican(settings=settings) + mute(True)(pelican.run)() + + for fname in ['pictures/Fat_Cat.jpg', 'pictures/Sushi_Macro.jpg']: + src = os.path.join(INPUT_PATH, fname) + dest = os.path.join(self.temp_path, fname) + self.assertTrue(os.path.exists(dest) and os.path.samefile(src, dest)) + def test_theme_static_paths_copy(self): # the same thing with a specified set of settings should work settings = read_settings(path=SAMPLE_CONFIG, override={ diff --git a/pelican/utils.py b/pelican/utils.py index 2af34ecf..32be4b9b 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -229,7 +229,12 @@ def slugify(value, substitutions=()): return value.decode('ascii') -def copy(source, destination): +_LINK_FUNCS = { # map: link type -> func name in os module + 'hard': 'link', + 'symbolic': 'symlink', + } + +def copy(source, destination, just_link=''): """Recursively copy source into destination. If source is a file, destination has to be a file as well. @@ -238,11 +243,40 @@ def copy(source, destination): :param source: the source file or directory :param destination: the destination file or directory + :param just_link: type of link to use instead of copying, + 'hard' or 'symbolic' """ source_ = os.path.abspath(os.path.expanduser(source)) destination_ = os.path.abspath(os.path.expanduser(destination)) + if just_link: + try: + dest = destination_ + link_func = getattr(os, _LINK_FUNCS[just_link]) + if just_link == 'symbolic' and six.PY3: + link_func = partial(link_func, + target_is_directory=os.path.isdir(source_)) + if os.path.exists(dest) and os.path.isdir(dest): + dest = os.path.join(dest, os.path.basename(source_)) + else: + dest_dir = os.path.dirname(dest) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + link_func(source_, dest) + logger.info('linking ({}) {} -> {}'.format( + just_link, source_, dest)) + return + except KeyError: + logger.error('Unknown link type: {}'.format(just_link)) + except AttributeError as err: + logger.error(('{} linking not supported by platform, ' + 'falling back to copying\n{}').format(just_link, err)) + except (OSError, IOError) as err: + logger.error(('Cannot make {} link {} -> {}, ' + 'falling back to copying\n{}').format( + just_link, source_, dest ,err)) + if not os.path.exists(destination_) and not os.path.isfile(source_): os.makedirs(destination_) @@ -684,4 +718,3 @@ def is_selected_for_writing(settings, path): return path in settings['WRITE_SELECTED'] else: return True - |