plone.exportimport 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plone/exportimport/__init__.py +8 -0
- plone/exportimport/cli/__init__.py +78 -0
- plone/exportimport/configure.zcml +21 -0
- plone/exportimport/deserializers/__init__.py +0 -0
- plone/exportimport/deserializers/blob.py +44 -0
- plone/exportimport/deserializers/blocks.py +15 -0
- plone/exportimport/deserializers/configure.zcml +4 -0
- plone/exportimport/exporters/__init__.py +85 -0
- plone/exportimport/exporters/base.py +80 -0
- plone/exportimport/exporters/configure.zcml +61 -0
- plone/exportimport/exporters/content.py +165 -0
- plone/exportimport/exporters/discussions.py +34 -0
- plone/exportimport/exporters/portlets.py +19 -0
- plone/exportimport/exporters/principals.py +24 -0
- plone/exportimport/exporters/redirects.py +19 -0
- plone/exportimport/exporters/relations.py +19 -0
- plone/exportimport/exporters/translations.py +21 -0
- plone/exportimport/importers/__init__.py +66 -0
- plone/exportimport/importers/base.py +93 -0
- plone/exportimport/importers/configure.zcml +67 -0
- plone/exportimport/importers/content.py +166 -0
- plone/exportimport/importers/discussions.py +24 -0
- plone/exportimport/importers/final.py +44 -0
- plone/exportimport/importers/portlets.py +16 -0
- plone/exportimport/importers/principals.py +34 -0
- plone/exportimport/importers/redirects.py +16 -0
- plone/exportimport/importers/relations.py +23 -0
- plone/exportimport/importers/translations.py +16 -0
- plone/exportimport/interfaces.py +23 -0
- plone/exportimport/serializers/__init__.py +0 -0
- plone/exportimport/serializers/blob.py +100 -0
- plone/exportimport/serializers/blocks.py +15 -0
- plone/exportimport/serializers/configure.zcml +10 -0
- plone/exportimport/serializers/fields.py +93 -0
- plone/exportimport/settings.py +19 -0
- plone/exportimport/testing/__init__.py +27 -0
- plone/exportimport/types.py +71 -0
- plone/exportimport/utils/__init__.py +1 -0
- plone/exportimport/utils/cli.py +63 -0
- plone/exportimport/utils/content/__init__.py +19 -0
- plone/exportimport/utils/content/blocks.py +47 -0
- plone/exportimport/utils/content/core.py +81 -0
- plone/exportimport/utils/content/export_helpers.py +365 -0
- plone/exportimport/utils/content/import_helpers.py +382 -0
- plone/exportimport/utils/content/revisions.py +45 -0
- plone/exportimport/utils/dates.py +21 -0
- plone/exportimport/utils/discussions.py +137 -0
- plone/exportimport/utils/path.py +21 -0
- plone/exportimport/utils/permissions.py +49 -0
- plone/exportimport/utils/portlets.py +312 -0
- plone/exportimport/utils/principals/__init__.py +4 -0
- plone/exportimport/utils/principals/groups.py +50 -0
- plone/exportimport/utils/principals/helpers.py +41 -0
- plone/exportimport/utils/principals/members.py +152 -0
- plone/exportimport/utils/redirects.py +30 -0
- plone/exportimport/utils/relations.py +102 -0
- plone/exportimport/utils/translations.py +143 -0
- plone/exportimport/utils/zca.py +13 -0
- plone.exportimport-1.0.0-py3.13-nspkg.pth +1 -0
- plone.exportimport-1.0.0.dist-info/LICENSE +339 -0
- plone.exportimport-1.0.0.dist-info/METADATA +316 -0
- plone.exportimport-1.0.0.dist-info/RECORD +66 -0
- plone.exportimport-1.0.0.dist-info/WHEEL +5 -0
- plone.exportimport-1.0.0.dist-info/entry_points.txt +6 -0
- plone.exportimport-1.0.0.dist-info/namespace_packages.txt +1 -0
- plone.exportimport-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from plone import api
|
|
2
|
+
from plone.exportimport.exporters import get_exporter
|
|
3
|
+
from plone.exportimport.importers import get_importer
|
|
4
|
+
from plone.exportimport.utils import cli as cli_helpers
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
import transaction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CLI_SPEC = {
|
|
12
|
+
"exporter": {
|
|
13
|
+
"description": "Export Plone Site content",
|
|
14
|
+
"options": {
|
|
15
|
+
"zopeconf": "Path to zope.conf",
|
|
16
|
+
"site": "Plone site ID to export the content from",
|
|
17
|
+
"path": "Path to export the content",
|
|
18
|
+
"--include-revisions": "Include revision history",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
"importer": {
|
|
22
|
+
"description": "Import content into a Plone Site",
|
|
23
|
+
"options": {
|
|
24
|
+
"zopeconf": "Path to zope.conf",
|
|
25
|
+
"site": "Plone site ID to import the content to",
|
|
26
|
+
"path": "Path to import the content from",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_args(description: str, options: dict, args: list):
|
|
33
|
+
parser = argparse.ArgumentParser(description=description)
|
|
34
|
+
for key, help in options.items():
|
|
35
|
+
if key.startswith("-"):
|
|
36
|
+
parser.add_argument(key, action="store_true", help=help)
|
|
37
|
+
else:
|
|
38
|
+
parser.add_argument(key, help=help)
|
|
39
|
+
namespace, _ = parser.parse_known_args(args[1:])
|
|
40
|
+
return namespace
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def exporter_cli(args=sys.argv):
|
|
44
|
+
"""Export a Plone site."""
|
|
45
|
+
logger = cli_helpers.get_logger("Exporter")
|
|
46
|
+
exporter_cli = CLI_SPEC["exporter"]
|
|
47
|
+
# We get an argparse.Namespace instance.
|
|
48
|
+
namespace = _parse_args(exporter_cli["description"], exporter_cli["options"], args)
|
|
49
|
+
app = cli_helpers.get_app(namespace.zopeconf)
|
|
50
|
+
path = cli_helpers._process_path(namespace.path)
|
|
51
|
+
if not path:
|
|
52
|
+
logger.error(f"{namespace.path} does not exist, please create it first.")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
site = cli_helpers.get_site(app, namespace.site, logger)
|
|
55
|
+
with api.env.adopt_roles(["Manager"]):
|
|
56
|
+
results = get_exporter(site).export_site(path, options=namespace)
|
|
57
|
+
logger.info(f" Using path {path} to export content from Plone site at /{site.id}")
|
|
58
|
+
for item in results[1:]:
|
|
59
|
+
logger.info(f" Wrote {item}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def importer_cli(args=sys.argv):
|
|
63
|
+
"""Import content to a Plone site."""
|
|
64
|
+
logger = cli_helpers.get_logger("Importer")
|
|
65
|
+
importer_cli = CLI_SPEC["importer"]
|
|
66
|
+
namespace = _parse_args(importer_cli["description"], importer_cli["options"], args)
|
|
67
|
+
app = cli_helpers.get_app(namespace.zopeconf)
|
|
68
|
+
path = cli_helpers._process_path(namespace.path)
|
|
69
|
+
if not path:
|
|
70
|
+
logger.error(f"{namespace.path} does not exist, aborting import.")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
site = cli_helpers.get_site(app, namespace.site, logger)
|
|
73
|
+
with api.env.adopt_roles(["Manager"]):
|
|
74
|
+
results = get_importer(site).import_site(path)
|
|
75
|
+
logger.info(f" Using path {path} to import content to Plone site at /{site.id}")
|
|
76
|
+
for item in results:
|
|
77
|
+
logger.info(f" - {item}")
|
|
78
|
+
transaction.commit()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<configure
|
|
2
|
+
xmlns="http://namespaces.zope.org/zope"
|
|
3
|
+
xmlns:i18n="http://namespaces.zope.org/i18n"
|
|
4
|
+
xmlns:zcml="http://namespaces.zope.org/zcml"
|
|
5
|
+
i18n_domain="plone"
|
|
6
|
+
>
|
|
7
|
+
|
|
8
|
+
<!-- Dependencies -->
|
|
9
|
+
<include package="Products.CMFCore" />
|
|
10
|
+
<include package="Products.CMFPlone" />
|
|
11
|
+
<include package="plone.namedfile" />
|
|
12
|
+
<include package="plone.app.dexterity" />
|
|
13
|
+
<include package="plone.restapi" />
|
|
14
|
+
|
|
15
|
+
<include package=".serializers" />
|
|
16
|
+
<include package=".deserializers" />
|
|
17
|
+
<include package=".exporters" />
|
|
18
|
+
<include package=".importers" />
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
</configure>
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from plone.dexterity.interfaces import IDexterityContent
|
|
3
|
+
from plone.exportimport import settings
|
|
4
|
+
from plone.exportimport.interfaces import IExportImportRequestMarker
|
|
5
|
+
from plone.exportimport.utils import path as path_utils
|
|
6
|
+
from plone.namedfile.interfaces import INamedField
|
|
7
|
+
from plone.restapi.deserializer.dxfields import DefaultFieldDeserializer
|
|
8
|
+
from plone.restapi.interfaces import IFieldDeserializer
|
|
9
|
+
from zope.component import adapter
|
|
10
|
+
from zope.globalrequest import getRequest
|
|
11
|
+
from zope.interface import implementer
|
|
12
|
+
|
|
13
|
+
import codecs
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_blob(path: str) -> bytes:
|
|
17
|
+
"""Load blob from fs and encode it as base64."""
|
|
18
|
+
request = getRequest()
|
|
19
|
+
content_import_path = Path(request[settings.IMPORT_PATH_KEY])
|
|
20
|
+
path = content_import_path / path
|
|
21
|
+
if not path.exists():
|
|
22
|
+
raise ValueError(f"Blob not found at {path}")
|
|
23
|
+
data = path_utils.encode_file_contents(path)
|
|
24
|
+
return codecs.decode(data, "base64")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@adapter(INamedField, IDexterityContent, IExportImportRequestMarker)
|
|
28
|
+
@implementer(IFieldDeserializer)
|
|
29
|
+
class ExportImportNamedFieldDeserializer(DefaultFieldDeserializer):
|
|
30
|
+
def __call__(self, value):
|
|
31
|
+
result = None
|
|
32
|
+
blob_path = value.pop("blob_path", "")
|
|
33
|
+
content_type = value.get("content-type", "application/octet-stream")
|
|
34
|
+
filename = value.get("filename", None)
|
|
35
|
+
data = load_blob(blob_path)
|
|
36
|
+
# Convert if we have data
|
|
37
|
+
if data:
|
|
38
|
+
result = self.field._type(
|
|
39
|
+
data=data, contentType=content_type, filename=filename
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Always validate to check for required fields
|
|
43
|
+
self.field.validate(result)
|
|
44
|
+
return result
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from plone.exportimport.interfaces import IExportImportRequestMarker
|
|
2
|
+
from plone.restapi.behaviors import IBlocks
|
|
3
|
+
from plone.restapi.deserializer.dxfields import DefaultFieldDeserializer
|
|
4
|
+
from plone.restapi.interfaces import IFieldDeserializer
|
|
5
|
+
from plone.schema import IJSONField
|
|
6
|
+
from zope.component import adapter
|
|
7
|
+
from zope.interface import implementer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@implementer(IFieldDeserializer)
|
|
11
|
+
@adapter(IJSONField, IBlocks, IExportImportRequestMarker)
|
|
12
|
+
class ExportImportBlocksDeserializer(DefaultFieldDeserializer):
|
|
13
|
+
"""We skip the subscribers that deserialize the blocks from the frontend.
|
|
14
|
+
We only need the raw data.
|
|
15
|
+
"""
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from .base import BaseExporter
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from plone import api
|
|
4
|
+
from plone.exportimport import interfaces
|
|
5
|
+
from plone.exportimport import logger
|
|
6
|
+
from plone.exportimport import PACKAGE_NAME
|
|
7
|
+
from Products.CMFPlone.Portal import PloneSite
|
|
8
|
+
from tempfile import mkdtemp
|
|
9
|
+
from typing import Dict
|
|
10
|
+
from typing import List
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from zope.component import getAdapter
|
|
13
|
+
from zope.component import hooks
|
|
14
|
+
from zope.component import queryAdapter
|
|
15
|
+
from zope.interface import implementer
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
EXPORTER_NAMES = [
|
|
21
|
+
"plone.exporter.content",
|
|
22
|
+
"plone.exporter.principals",
|
|
23
|
+
"plone.exporter.redirects",
|
|
24
|
+
"plone.exporter.relations",
|
|
25
|
+
"plone.exporter.translations",
|
|
26
|
+
"plone.exporter.discussions",
|
|
27
|
+
"plone.exporter.portlets",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ExporterMapping = Dict[str, BaseExporter]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@implementer(interfaces.IExporter)
|
|
35
|
+
class Exporter:
|
|
36
|
+
"""Export content from a Plone Site."""
|
|
37
|
+
|
|
38
|
+
exporters: ExporterMapping
|
|
39
|
+
|
|
40
|
+
def __init__(self, site):
|
|
41
|
+
self.site = site
|
|
42
|
+
self.exporters = self.all_exporters()
|
|
43
|
+
|
|
44
|
+
def all_exporters(self) -> ExporterMapping:
|
|
45
|
+
"""Return all exporters."""
|
|
46
|
+
exporters = {}
|
|
47
|
+
for exporter_name in EXPORTER_NAMES:
|
|
48
|
+
exporter = queryAdapter(
|
|
49
|
+
self.site, interfaces.INamedExporter, name=exporter_name
|
|
50
|
+
)
|
|
51
|
+
if exporter:
|
|
52
|
+
exporters[exporter_name] = exporter
|
|
53
|
+
return exporters
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _prepare_path(path: Optional[Path] = None) -> Path:
|
|
57
|
+
"""Return a valid path to use for the export.
|
|
58
|
+
|
|
59
|
+
If base_path is not given, create a temporary directory to export
|
|
60
|
+
the content.
|
|
61
|
+
"""
|
|
62
|
+
valid_path = path.is_dir() if path else False
|
|
63
|
+
if not valid_path:
|
|
64
|
+
path = Path(mkdtemp(prefix=PACKAGE_NAME))
|
|
65
|
+
return path
|
|
66
|
+
|
|
67
|
+
def export_site(
|
|
68
|
+
self, path: Optional[Path] = None, options: Optional[argparse.Namespace] = None
|
|
69
|
+
) -> List[Path]:
|
|
70
|
+
"""Export the given site to the filesystem."""
|
|
71
|
+
path = self._prepare_path(path)
|
|
72
|
+
paths: List[Path] = [path]
|
|
73
|
+
with hooks.site(self.site):
|
|
74
|
+
for exporter_name, exporter in self.exporters.items():
|
|
75
|
+
logger.debug(f"Exporting {self.site} with {exporter_name} to {path}")
|
|
76
|
+
new_paths = exporter.export_data(path, options=options)
|
|
77
|
+
paths.extend(new_paths)
|
|
78
|
+
return paths
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_exporter(site: PloneSite = None) -> Exporter:
|
|
82
|
+
"""Get the exporter."""
|
|
83
|
+
if site is None:
|
|
84
|
+
site = api.portal.get()
|
|
85
|
+
return getAdapter(site, interfaces.IExporter)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from plone.exportimport import types
|
|
3
|
+
from plone.exportimport.utils import content as utils
|
|
4
|
+
from plone.exportimport.utils import path as path_utils
|
|
5
|
+
from Products.CMFPlone.Portal import PloneSite
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import Callable
|
|
8
|
+
from typing import List
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from zope.globalrequest import getRequest
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseExporter:
|
|
17
|
+
name: str
|
|
18
|
+
base_path: Path
|
|
19
|
+
errors: list = None
|
|
20
|
+
request: types.HTTPRequest = None
|
|
21
|
+
data_hooks: List[Callable] = None
|
|
22
|
+
obj_hooks: List[Callable] = None
|
|
23
|
+
options: Optional[argparse.Namespace] = None
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
site: PloneSite = None,
|
|
28
|
+
):
|
|
29
|
+
self.site = site
|
|
30
|
+
self.errors = []
|
|
31
|
+
self.request = getRequest()
|
|
32
|
+
|
|
33
|
+
def get_option(self, name, default=None):
|
|
34
|
+
return getattr(self.options, name, default)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def filepath(self) -> Path:
|
|
38
|
+
"""Filepath to be used during export."""
|
|
39
|
+
filename = f"{self.name}.json"
|
|
40
|
+
return self.base_path / filename
|
|
41
|
+
|
|
42
|
+
def _serializer(self, obj: Any) -> Callable:
|
|
43
|
+
"""Serializer for object."""
|
|
44
|
+
return utils.get_serializer(obj, self.request)
|
|
45
|
+
|
|
46
|
+
def _dump(self, data: dict, filepath: Path = None) -> Path:
|
|
47
|
+
"""Dump serialized data to disk."""
|
|
48
|
+
filepath = filepath if filepath else self.filepath
|
|
49
|
+
# Create container, if it does not exist
|
|
50
|
+
path_utils.get_parent_folder(filepath)
|
|
51
|
+
with open(filepath, "w") as fh:
|
|
52
|
+
json.dump(data, fh, indent=2, sort_keys=True)
|
|
53
|
+
# json.dump does not add a newline at the end of the file, so we
|
|
54
|
+
# explicitly do it. Otherwise when you manually edit a file and
|
|
55
|
+
# you use an editor that respects the standard `.editorconfig`
|
|
56
|
+
# that we have in most Plone packages, you always get a diff because
|
|
57
|
+
# your editor has automatically added a newline at the end.
|
|
58
|
+
fh.write("\n")
|
|
59
|
+
return filepath
|
|
60
|
+
|
|
61
|
+
def dump(self) -> List[Path]:
|
|
62
|
+
"""Serialize objects."""
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
def export_data(
|
|
66
|
+
self,
|
|
67
|
+
base_path: Path,
|
|
68
|
+
data_hooks: List[Callable] = None,
|
|
69
|
+
obj_hooks: List[Callable] = None,
|
|
70
|
+
options: Optional[argparse.Namespace] = None,
|
|
71
|
+
) -> List[Path]:
|
|
72
|
+
"""Write data to filesystem."""
|
|
73
|
+
if not base_path.exists():
|
|
74
|
+
base_path.mkdir(parents=True)
|
|
75
|
+
self.base_path = base_path
|
|
76
|
+
self.data_hooks = self.data_hooks or data_hooks or []
|
|
77
|
+
self.obj_hooks = self.obj_hooks or obj_hooks or []
|
|
78
|
+
self.options = options
|
|
79
|
+
paths = self.dump()
|
|
80
|
+
return paths
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<configure
|
|
2
|
+
xmlns="http://namespaces.zope.org/zope"
|
|
3
|
+
xmlns:zcml="http://namespaces.zope.org/zcml"
|
|
4
|
+
>
|
|
5
|
+
<!-- Main exporter -->
|
|
6
|
+
<adapter
|
|
7
|
+
factory=".Exporter"
|
|
8
|
+
for="plone.base.interfaces.siteroot.IPloneSiteRoot"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<!-- Exporters -->
|
|
12
|
+
<adapter
|
|
13
|
+
factory=".content.ContentExporter"
|
|
14
|
+
provides="plone.exportimport.interfaces.INamedExporter"
|
|
15
|
+
for="plone.base.interfaces.siteroot.IPloneSiteRoot"
|
|
16
|
+
name="plone.exporter.content"
|
|
17
|
+
/>
|
|
18
|
+
<adapter
|
|
19
|
+
factory=".principals.PrincipalsExporter"
|
|
20
|
+
provides="plone.exportimport.interfaces.INamedExporter"
|
|
21
|
+
for="plone.base.interfaces.siteroot.IPloneSiteRoot"
|
|
22
|
+
name="plone.exporter.principals"
|
|
23
|
+
/>
|
|
24
|
+
<adapter
|
|
25
|
+
factory=".redirects.RedirectsExporter"
|
|
26
|
+
provides="plone.exportimport.interfaces.INamedExporter"
|
|
27
|
+
for="plone.base.interfaces.siteroot.IPloneSiteRoot"
|
|
28
|
+
name="plone.exporter.redirects"
|
|
29
|
+
/>
|
|
30
|
+
<adapter
|
|
31
|
+
factory=".relations.RelationsExporter"
|
|
32
|
+
provides="plone.exportimport.interfaces.INamedExporter"
|
|
33
|
+
for="plone.base.interfaces.siteroot.IPloneSiteRoot"
|
|
34
|
+
name="plone.exporter.relations"
|
|
35
|
+
/>
|
|
36
|
+
<configure zcml:condition="installed plone.app.multilingual">
|
|
37
|
+
<adapter
|
|
38
|
+
factory=".translations.TranslationsExporter"
|
|
39
|
+
provides="plone.exportimport.interfaces.INamedExporter"
|
|
40
|
+
for="plone.base.interfaces.siteroot.IPloneSiteRoot"
|
|
41
|
+
name="plone.exporter.translations"
|
|
42
|
+
/>
|
|
43
|
+
</configure>
|
|
44
|
+
<configure zcml:condition="installed plone.app.discussion">
|
|
45
|
+
<adapter
|
|
46
|
+
factory=".discussions.DiscussionsExporter"
|
|
47
|
+
provides="plone.exportimport.interfaces.INamedExporter"
|
|
48
|
+
for="plone.base.interfaces.siteroot.IPloneSiteRoot"
|
|
49
|
+
name="plone.exporter.discussions"
|
|
50
|
+
/>
|
|
51
|
+
</configure>
|
|
52
|
+
<configure zcml:condition="installed plone.app.portlets">
|
|
53
|
+
<adapter
|
|
54
|
+
factory=".portlets.PortletsExporter"
|
|
55
|
+
provides="plone.exportimport.interfaces.INamedExporter"
|
|
56
|
+
for="plone.base.interfaces.siteroot.IPloneSiteRoot"
|
|
57
|
+
name="plone.exporter.portlets"
|
|
58
|
+
/>
|
|
59
|
+
</configure>
|
|
60
|
+
|
|
61
|
+
</configure>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from .base import BaseExporter
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from plone import api
|
|
4
|
+
from plone.base.interfaces import IPloneSiteRoot
|
|
5
|
+
from plone.dexterity.content import DexterityContent
|
|
6
|
+
from plone.exportimport import interfaces
|
|
7
|
+
from plone.exportimport import logger
|
|
8
|
+
from plone.exportimport import settings
|
|
9
|
+
from plone.exportimport import types
|
|
10
|
+
from plone.exportimport.interfaces import IExportImportRequestMarker
|
|
11
|
+
from plone.exportimport.utils import content as content_utils
|
|
12
|
+
from plone.exportimport.utils import request_provides
|
|
13
|
+
from typing import Callable
|
|
14
|
+
from typing import Generator
|
|
15
|
+
from typing import List
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from zope.interface import implementer
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@implementer(interfaces.INamedExporter)
|
|
23
|
+
class ContentExporter(BaseExporter):
|
|
24
|
+
name: str = "content"
|
|
25
|
+
query: dict = None
|
|
26
|
+
filename_fmt: str = settings.EXPORT_CONTENT_FILEPATH
|
|
27
|
+
metadata: types.ExportImportMetadata = None
|
|
28
|
+
default_site_language: str = "en"
|
|
29
|
+
|
|
30
|
+
def all_objects(self) -> Generator:
|
|
31
|
+
"""Return all objects to be serialized."""
|
|
32
|
+
query = self.query
|
|
33
|
+
catalog = api.portal.get_tool("portal_catalog")
|
|
34
|
+
if "object_provides" not in query:
|
|
35
|
+
query["object_provides"] = "plone.dexterity.interfaces.IDexterityContent"
|
|
36
|
+
brains = catalog.unrestrictedSearchResults(**query)
|
|
37
|
+
logger.info(f"Exporting {len(brains)}")
|
|
38
|
+
for index, brain in enumerate(brains, start=1):
|
|
39
|
+
try:
|
|
40
|
+
obj = brain.getObject()
|
|
41
|
+
except Exception:
|
|
42
|
+
brain_path = brain.getPath()
|
|
43
|
+
msg = f"Error getting object {brain_path} from brain"
|
|
44
|
+
self.errors.append({"path": brain_path, "message": msg})
|
|
45
|
+
logger.exception(msg, exc_info=True)
|
|
46
|
+
else:
|
|
47
|
+
yield obj
|
|
48
|
+
|
|
49
|
+
if not index % 100:
|
|
50
|
+
logger.info(f"Handled {index} items...")
|
|
51
|
+
|
|
52
|
+
def serialize(self, obj: DexterityContent) -> dict:
|
|
53
|
+
"""Serialize object."""
|
|
54
|
+
obj_uid = content_utils.get_uid(obj)
|
|
55
|
+
obj_path = content_utils.get_obj_path(obj)
|
|
56
|
+
config = types.ExporterConfig(
|
|
57
|
+
site=self.site,
|
|
58
|
+
site_root_uid=self.site.UID(),
|
|
59
|
+
request=self.request,
|
|
60
|
+
serializer=self._serializer(obj),
|
|
61
|
+
logger_prefix=f"- {obj_path}:",
|
|
62
|
+
)
|
|
63
|
+
kwargs = {}
|
|
64
|
+
is_site_root = IPloneSiteRoot.providedBy(obj)
|
|
65
|
+
is_folderish = content_utils.is_folderish(obj)
|
|
66
|
+
if is_folderish:
|
|
67
|
+
if not is_site_root:
|
|
68
|
+
kwargs["include_items"] = False
|
|
69
|
+
# Process metadata
|
|
70
|
+
for helper in content_utils.metadata_helpers():
|
|
71
|
+
logger.debug(f"{config.logger_prefix} Updating metadata {helper.name}")
|
|
72
|
+
value = helper.func(obj, config)
|
|
73
|
+
if value is not None:
|
|
74
|
+
getattr(self.metadata, helper.name)[obj_uid] = value
|
|
75
|
+
|
|
76
|
+
# Apply object hooks
|
|
77
|
+
for func in self.obj_hooks:
|
|
78
|
+
logger.debug(
|
|
79
|
+
f"{config.logger_prefix} Running object hook {func.__name__} on object"
|
|
80
|
+
)
|
|
81
|
+
obj = func(obj, config)
|
|
82
|
+
|
|
83
|
+
# Serialize
|
|
84
|
+
data = config.serializer(**kwargs)
|
|
85
|
+
|
|
86
|
+
# Fix serialized data
|
|
87
|
+
for fixer in content_utils.fixers():
|
|
88
|
+
logger.debug(
|
|
89
|
+
f"{config.logger_prefix} Running {fixer.name} on serialized data"
|
|
90
|
+
)
|
|
91
|
+
data = fixer.func(data, obj, config)
|
|
92
|
+
|
|
93
|
+
# Enrich
|
|
94
|
+
for enricher in content_utils.enrichers(
|
|
95
|
+
include_revisions=self.get_option("include_revisions")
|
|
96
|
+
):
|
|
97
|
+
logger.debug(f"{config.logger_prefix} Running {enricher.name}")
|
|
98
|
+
additional = enricher.func(obj, config)
|
|
99
|
+
if additional:
|
|
100
|
+
data.update(additional)
|
|
101
|
+
|
|
102
|
+
# Apply data hooks
|
|
103
|
+
for func in self.data_hooks:
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"{config.logger_prefix} Running data hook {func.__name__} on payload"
|
|
106
|
+
)
|
|
107
|
+
data = func(data, obj, config)
|
|
108
|
+
|
|
109
|
+
# Cleanup data
|
|
110
|
+
for cleaner in content_utils.cleaners():
|
|
111
|
+
logger.debug(
|
|
112
|
+
f"{config.logger_prefix} Running {cleaner.name} on serialized data"
|
|
113
|
+
)
|
|
114
|
+
data = cleaner.func(data, config)
|
|
115
|
+
return data
|
|
116
|
+
|
|
117
|
+
def dump_one(self, obj: DexterityContent) -> Path:
|
|
118
|
+
"""Serialize object and dump it to disk."""
|
|
119
|
+
obj_path = content_utils.get_obj_path(obj)
|
|
120
|
+
# Serialize object
|
|
121
|
+
data = self.serialize(obj)
|
|
122
|
+
base_path = self.base_path
|
|
123
|
+
filepath = base_path / self.filename_fmt.format(**data)
|
|
124
|
+
filepath = self._dump(data, filepath)
|
|
125
|
+
logger.debug(f"- {obj_path}: Wrote serialized data to {filepath}")
|
|
126
|
+
# Add to list of files
|
|
127
|
+
self.metadata._all_[obj_path] = str(filepath.relative_to(base_path))
|
|
128
|
+
return filepath
|
|
129
|
+
|
|
130
|
+
def dump_metadata(self) -> Path:
|
|
131
|
+
metadata = self.metadata.export()
|
|
132
|
+
filepath = self.base_path / "__metadata__.json"
|
|
133
|
+
return self._dump(metadata, filepath)
|
|
134
|
+
|
|
135
|
+
def dump(self) -> List[Path]:
|
|
136
|
+
"""Serialize contents and dump them to disk."""
|
|
137
|
+
paths = []
|
|
138
|
+
with request_provides(self.request, IExportImportRequestMarker):
|
|
139
|
+
for obj in self.all_objects():
|
|
140
|
+
path = self.dump_one(obj)
|
|
141
|
+
if path:
|
|
142
|
+
paths.append(path)
|
|
143
|
+
# Add list of blobs to serialization
|
|
144
|
+
paths.insert(0, self.dump_metadata())
|
|
145
|
+
return paths
|
|
146
|
+
|
|
147
|
+
def export_data(
|
|
148
|
+
self,
|
|
149
|
+
base_path: Path,
|
|
150
|
+
data_hooks: List[Callable] = None,
|
|
151
|
+
obj_hooks: List[Callable] = None,
|
|
152
|
+
query: Optional[dict] = None,
|
|
153
|
+
options: Optional[argparse.Namespace] = None,
|
|
154
|
+
) -> List[Path]:
|
|
155
|
+
# Content in a subpath of base_path
|
|
156
|
+
base_path = base_path / self.name
|
|
157
|
+
query = query if query else {}
|
|
158
|
+
site = self.site
|
|
159
|
+
self.query = query if query else {"path": content_utils.get_obj_path(site)}
|
|
160
|
+
metadata = types.ExportImportMetadata()
|
|
161
|
+
self.metadata = metadata
|
|
162
|
+
self.request[settings.EXPORT_CONTENT_METADATA_KEY] = metadata
|
|
163
|
+
self.request[settings.EXPORT_PATH_KEY] = base_path
|
|
164
|
+
self.default_site_language = site.language
|
|
165
|
+
return super().export_data(base_path, data_hooks, obj_hooks, options=options)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from .base import BaseExporter
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from plone.exportimport import interfaces
|
|
4
|
+
from plone.exportimport import logger
|
|
5
|
+
from typing import List
|
|
6
|
+
from zope.interface import implementer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import plone.app.discussion # noqa
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_DISCUSSION = False
|
|
13
|
+
else:
|
|
14
|
+
HAS_DISCUSSION = True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@implementer(interfaces.INamedExporter)
|
|
18
|
+
class DiscussionsExporter(BaseExporter):
|
|
19
|
+
name: str = "discussions"
|
|
20
|
+
|
|
21
|
+
def dump(self) -> List[Path]:
|
|
22
|
+
"""Serialize object and dump it to disk."""
|
|
23
|
+
if not HAS_DISCUSSION:
|
|
24
|
+
logger.debug("- Discussions: Skipping (plone.app.discussion not installed)")
|
|
25
|
+
return []
|
|
26
|
+
|
|
27
|
+
from plone.exportimport.utils import discussions as utils
|
|
28
|
+
|
|
29
|
+
discussions = utils.get_discussions()
|
|
30
|
+
filepath = self._dump(discussions, self.filepath)
|
|
31
|
+
logger.debug(
|
|
32
|
+
f"- Discussions: Wrote {len(discussions)} discussions to {filepath}"
|
|
33
|
+
)
|
|
34
|
+
return [filepath]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .base import BaseExporter
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from plone.exportimport import interfaces
|
|
4
|
+
from plone.exportimport import logger
|
|
5
|
+
from plone.exportimport.utils import portlets as utils
|
|
6
|
+
from typing import List
|
|
7
|
+
from zope.interface import implementer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@implementer(interfaces.INamedExporter)
|
|
11
|
+
class PortletsExporter(BaseExporter):
|
|
12
|
+
name: str = "portlets"
|
|
13
|
+
|
|
14
|
+
def dump(self) -> List[Path]:
|
|
15
|
+
"""Serialize object and dump it to disk."""
|
|
16
|
+
portlets = utils.get_portlets()
|
|
17
|
+
filepath = self._dump(portlets, self.filepath)
|
|
18
|
+
logger.debug(f"- Portlets: Wrote {len(portlets)} portlets to {filepath}")
|
|
19
|
+
return [filepath]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .base import BaseExporter
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from plone.exportimport import interfaces
|
|
4
|
+
from plone.exportimport import logger
|
|
5
|
+
from plone.exportimport.utils import principals as principals_utils
|
|
6
|
+
from typing import List
|
|
7
|
+
from zope.interface import implementer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@implementer(interfaces.INamedExporter)
|
|
11
|
+
class PrincipalsExporter(BaseExporter):
|
|
12
|
+
name: str = "principals"
|
|
13
|
+
|
|
14
|
+
def dump(self) -> List[Path]:
|
|
15
|
+
"""Serialize object and dump it to disk."""
|
|
16
|
+
data = {
|
|
17
|
+
"groups": principals_utils.export_groups(),
|
|
18
|
+
"members": principals_utils.export_members(),
|
|
19
|
+
}
|
|
20
|
+
filepath = self._dump(data, self.filepath)
|
|
21
|
+
logger.debug(
|
|
22
|
+
f"- Principals: Wrote {len(data['groups'])} groups and {len(data['members'])} members to {filepath}"
|
|
23
|
+
)
|
|
24
|
+
return [filepath]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .base import BaseExporter
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from plone.exportimport import interfaces
|
|
4
|
+
from plone.exportimport import logger
|
|
5
|
+
from plone.exportimport.utils import redirects as utils
|
|
6
|
+
from typing import List
|
|
7
|
+
from zope.interface import implementer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@implementer(interfaces.INamedExporter)
|
|
11
|
+
class RedirectsExporter(BaseExporter):
|
|
12
|
+
name: str = "redirects"
|
|
13
|
+
|
|
14
|
+
def dump(self) -> List[Path]:
|
|
15
|
+
"""Serialize object and dump it to disk."""
|
|
16
|
+
redirects = utils.get_redirects()
|
|
17
|
+
filepath = self._dump(redirects, self.filepath)
|
|
18
|
+
logger.debug(f"- Redirects: Wrote {len(redirects)} redirects to {filepath}")
|
|
19
|
+
return [filepath]
|