diff piecrust/publishing/base.py @ 885:13e8b50a2113

publish: Fix publishers API and add a simple "copy" publisher.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 20 Jun 2017 21:12:35 -0700
parents fd694f1297c7
children d709429f02eb
line wrap: on
line diff
--- a/piecrust/publishing/base.py	Tue Jun 20 21:10:39 2017 -0700
+++ b/piecrust/publishing/base.py	Tue Jun 20 21:12:35 2017 -0700
@@ -1,10 +1,7 @@
 import os.path
-import shlex
-import urllib.parse
+import time
 import logging
-import threading
-import subprocess
-from piecrust.configuration import try_get_dict_value
+from piecrust.chefutil import format_timed
 
 
 logger = logging.getLogger(__name__)
@@ -18,17 +15,17 @@
     pass
 
 
-class PublishingContext(object):
+class PublishingContext:
     def __init__(self):
         self.bake_out_dir = None
-        self.bake_record = None
+        self.bake_records = None
         self.processing_record = None
         self.was_baked = False
         self.preview = False
         self.args = None
 
 
-class Publisher(object):
+class Publisher:
     PUBLISHER_NAME = 'undefined'
     PUBLISHER_SCHEME = None
 
@@ -36,90 +33,129 @@
         self.app = app
         self.target = target
         self.config = config
-        self.has_url_config = isinstance(config, urllib.parse.ParseResult)
         self.log_file_path = None
 
     def setupPublishParser(self, parser, app):
         return
 
-    def getConfigValue(self, name, default_value=None):
-        if self.has_url_config:
-            raise Exception("This publisher only has a URL configuration.")
-        return try_get_dict_value(self.config, name, default=default_value)
+    def parseUrlTarget(self, url):
+        raise NotImplementedError()
 
     def run(self, ctx):
         raise NotImplementedError()
 
     def getBakedFiles(self, ctx):
-        for e in ctx.bake_record.entries:
-            for sub in e.subs:
-                if sub.was_baked:
-                    yield sub.out_path
-        for e in ctx.processing_record.entries:
-            if e.was_processed:
-                yield from [os.path.join(ctx.processing_record.out_dir, p)
-                        for p in e.rel_outputs]
+        for rec in ctx.bake_records.records:
+            for e in rec.getEntries():
+                paths = e.getAllOutputPaths()
+                if paths is not None:
+                    yield from paths
 
     def getDeletedFiles(self, ctx):
-        yield from ctx.bake_record.deleted
-        yield from ctx.processing_record.deleted
+        for rec in ctx.bake_records.records:
+            yield from rec.deleted_out_paths
+
+
+class InvalidPublishTargetError(Exception):
+    pass
+
+
+class PublishingError(Exception):
+    pass
 
 
-class ShellCommandPublisherBase(Publisher):
-    def __init__(self, app, target, config):
-        super(ShellCommandPublisherBase, self).__init__(app, target, config)
-        self.expand_user_args = True
+class PublishingManager:
+    def __init__(self, appfactory, app):
+        self.appfactory = appfactory
+        self.app = app
+
+    def run(self, target,
+            force=False, preview=False, extra_args=None, log_file=None):
+        start_time = time.perf_counter()
+
+        # Get publisher for this target.
+        pub = self.app.getPublisher(target)
+        if pub is None:
+            raise InvalidPublishTargetError(
+                "No such publish target: %s" % target)
+
+        # Will we need to bake first?
+        bake_first = pub.config.get('bake', True)
 
-    def run(self, ctx):
-        args = self._getCommandArgs(ctx)
-        if self.expand_user_args:
-            args = [os.path.expanduser(i) for i in args]
+        # Setup logging stuff.
+        hdlr = None
+        root_logger = logging.getLogger()
+        if log_file and not preview:
+            logger.debug("Adding file handler for: %s" % log_file)
+            hdlr = logging.FileHandler(log_file, mode='w', encoding='utf8')
+            root_logger.addHandler(hdlr)
+        if not preview:
+            logger.info("Deploying to %s" % target)
+        else:
+            logger.info("Previewing deployment to %s" % target)
 
-        if ctx.preview:
-            preview_args = ' '.join([shlex.quote(i) for i in args])
-            logger.info(
-                    "Would run shell command: %s" % preview_args)
-            return True
-
-        logger.debug(
-                "Running shell command: %s" % args)
+        # Bake first is necessary.
+        records = None
+        was_baked = False
+        bake_out_dir = os.path.join(self.app.root_dir, '_pub', target)
+        if bake_first:
+            if not preview:
+                bake_start_time = time.perf_counter()
+                logger.debug("Baking first to: %s" % bake_out_dir)
 
-        proc = subprocess.Popen(
-                args, cwd=self.app.root_dir, bufsize=0,
-                stdout=subprocess.PIPE)
+                from piecrust.baking.baker import Baker
+                baker = Baker(
+                    self.appfactory, self.app, bake_out_dir, force=force)
+                records = baker.bake()
+                was_baked = True
 
-        logger.debug("Running publishing monitor for PID %d" % proc.pid)
-        thread = _PublishThread(proc)
-        thread.start()
-        proc.wait()
-        thread.join()
+                if not records.success:
+                    raise Exception(
+                        "Error during baking, aborting publishing.")
+                logger.info(format_timed(bake_start_time, "Baked website."))
+            else:
+                logger.info("Would bake to: %s" % bake_out_dir)
+
+        # Publish!
+        logger.debug(
+            "Running publish target '%s' with publisher: %s" %
+            (target, pub.PUBLISHER_NAME))
+        pub_start_time = time.perf_counter()
 
-        if proc.returncode != 0:
-            logger.error(
-                    "Publish process returned code %d" % proc.returncode)
-        else:
-            logger.debug("Publish process returned successfully.")
+        ctx = PublishingContext()
+        ctx.bake_out_dir = bake_out_dir
+        ctx.bake_records = records
+        ctx.was_baked = was_baked
+        ctx.preview = preview
+        ctx.args = extra_args
+        try:
+            pub.run(ctx)
+        except Exception as ex:
+            raise PublishingError(
+                "Error publishing to target: %s" % target) from ex
+        finally:
+            if hdlr:
+                root_logger.removeHandler(hdlr)
+                hdlr.close()
 
-        return proc.returncode == 0
+        logger.info(format_timed(
+            pub_start_time, "Ran publisher %s" % pub.PUBLISHER_NAME))
 
-    def _getCommandArgs(self, ctx):
-        raise NotImplementedError()
+        logger.info(format_timed(start_time, 'Deployed to %s' % target))
 
 
-class _PublishThread(threading.Thread):
-    def __init__(self, proc):
-        super(_PublishThread, self).__init__(
-                name='publish_monitor', daemon=True)
-        self.proc = proc
-        self.root_logger = logging.getLogger()
+def find_publisher_class(app, name, is_scheme=False):
+    attr_name = 'PUBLISHER_SCHEME' if is_scheme else 'PUBLISHER_NAME'
+    for pub_cls in app.plugin_loader.getPublishers():
+        pub_sch = getattr(pub_cls, attr_name, None)
+        if pub_sch == name:
+            return pub_cls
+    return None
 
-    def run(self):
-        for line in iter(self.proc.stdout.readline, b''):
-            line_str = line.decode('utf8')
-            logger.info(line_str.rstrip('\r\n'))
-            for h in self.root_logger.handlers:
-                h.flush()
 
-        self.proc.communicate()
-        logger.debug("Publish monitor exiting.")
+def find_publisher_name(app, scheme):
+    pub_cls = find_publisher_class(app, scheme, True)
+    if pub_cls:
+        return pub_cls.PUBLISHER_NAME
+    return None