Office365-REST-Python-Client 3.1.0__py3-none-any.whl → 3.2.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.
- office365/communications/callrecords/call_record.py +18 -0
- office365/communications/calls/call.py +3 -0
- office365/communications/presences/presence.py +3 -0
- office365/communications/quotas.py +66 -0
- office365/communications/virtualevents/virtual_event.py +3 -0
- office365/directory/domains/domain.py +3 -0
- office365/directory/licenses/subscribed_sku.py +3 -0
- office365/directory/objects/object.py +3 -0
- office365/directory/quotas.py +34 -0
- office365/entity_collection.py +2 -2
- office365/graph_client.py +46 -2
- office365/limits.py +66 -0
- office365/migration/__init__.py +6 -1
- office365/migration/adapters/__init__.py +14 -4
- office365/migration/adapters/filesystem.py +4 -1
- office365/migration/assessment/export.py +5 -5
- office365/migration/assessment/issue.py +19 -0
- office365/migration/assessment/report.py +11 -0
- office365/migration/assessment/scanners/base.py +37 -49
- office365/migration/base.py +33 -8
- office365/migration/checkpoint.py +1 -1
- office365/migration/outlook/assessor.py +4 -4
- office365/migration/package/__init__.py +82 -0
- office365/migration/package/builder.py +603 -0
- office365/migration/package/crypto.py +54 -0
- office365/migration/package/manifest_xml.py +495 -0
- office365/migration/package/staging.py +180 -0
- office365/migration/report.py +7 -2
- office365/migration/report_io.py +79 -0
- office365/migration/runner.py +117 -10
- office365/migration/server_job.py +126 -14
- office365/migration/session.py +132 -49
- office365/migration/settings.py +67 -0
- office365/migration/sharepoint/__init__.py +2 -0
- office365/migration/sharepoint/adapters.py +207 -41
- office365/migration/sharepoint/assessor.py +45 -9
- office365/migration/sharepoint/options.py +57 -0
- office365/migration/sharepoint/package_target.py +209 -0
- office365/migration/sharepoint/registry.py +22 -0
- office365/migration/sharepoint/resolvers.py +96 -0
- office365/migration/sharepoint/scanners/__init__.py +29 -0
- office365/migration/sharepoint/scanners/browser_file_handling.py +55 -0
- office365/migration/sharepoint/scanners/checked_out_files.py +57 -0
- office365/migration/sharepoint/scanners/file_versions.py +56 -0
- office365/migration/sharepoint/scanners/files.py +5 -2
- office365/migration/sharepoint/scanners/large_excel_files.py +56 -0
- office365/migration/sharepoint/scanners/large_lists.py +58 -0
- office365/migration/sharepoint/scanners/large_sites.py +4 -3
- office365/migration/sharepoint/scanners/long_onedrive_urls.py +52 -0
- office365/migration/sharepoint/scanners/lookup.py +38 -0
- office365/migration/sharepoint/scanners/paths.py +9 -4
- office365/migration/sharepoint/scanners/permissions.py +20 -2
- office365/migration/sharepoint/scanners/smat.py +51 -0
- office365/migration/sharepoint/scanners/summary.py +1 -0
- office365/migration/sharepoint/scanners/thicket_folders.py +38 -0
- office365/migration/sharepoint/scanners/unsupported_site_templates.py +90 -0
- office365/migration/sharepoint/tenant_assessor.py +5 -3
- office365/migration/sharepoint/transfer.py +4 -2
- office365/migration/tasks.py +121 -0
- office365/migration/validators.py +2 -19
- office365/onedrive/driveitems/driveItem.py +116 -12
- office365/onedrive/termstore/groups/collection.py +2 -9
- office365/onedrive/termstore/sets/collection.py +2 -9
- office365/onedrive/termstore/terms/collection.py +3 -10
- office365/onedrive/workbooks/workbook.py +7 -0
- office365/outlook/contacts/folders/collection.py +4 -13
- office365/runtime/auth/authentication_context.py +5 -0
- office365/runtime/client_object.py +18 -0
- office365/runtime/client_object_collection.py +75 -245
- office365/runtime/client_request.py +77 -1
- office365/runtime/client_request_exception.py +127 -39
- office365/runtime/client_runtime_context.py +79 -8
- office365/runtime/converters/csv_writer.py +10 -4
- office365/runtime/converters/dataframe.py +84 -4
- office365/runtime/converters/json_file.py +12 -3
- office365/runtime/converters/parquet.py +67 -0
- office365/runtime/converters/records.py +31 -12
- office365/runtime/converters/registry.py +185 -0
- office365/runtime/converters/scalars.py +31 -0
- office365/runtime/converters/sql.py +69 -0
- office365/runtime/converters/streamers.py +105 -0
- office365/runtime/converters/streams.py +42 -0
- office365/runtime/converters/upsert.py +101 -0
- office365/runtime/exceptions.py +105 -0
- office365/runtime/http/prefer.py +26 -0
- office365/runtime/http/throttling.py +258 -56
- office365/runtime/imports.py +571 -0
- office365/runtime/limits.py +455 -0
- office365/runtime/odata/request.py +11 -2
- office365/runtime/odata/type.py +1 -0
- office365/runtime/odata/v3/batch_request.py +4 -2
- office365/runtime/odata/v4/batch_request.py +22 -7
- office365/runtime/operations.py +29 -0
- office365/runtime/parallel.py +12 -14
- office365/runtime/paths/v4/entity.py +2 -2
- office365/runtime/queries/batch.py +18 -1
- office365/runtime/queries/get_or_create.py +109 -0
- office365/runtime/record_collection.py +443 -0
- office365/runtime/retry.py +92 -26
- office365/runtime/transport/base.py +6 -1
- office365/runtime/transport/requests_transport.py +4 -0
- office365/runtime/transport/throttled_transport.py +67 -0
- office365/runtime/verification.py +40 -0
- office365/sharepoint/activities/logger.py +118 -8
- office365/sharepoint/administration/analytics/usage_service.py +15 -7
- office365/sharepoint/administration/sitemove/service.py +791 -8
- office365/sharepoint/alerts/alert.py +79 -0
- office365/sharepoint/apps/app_collection.py +25 -0
- office365/sharepoint/apps/launcher.py +16 -0
- office365/sharepoint/authpolicy/tenant/compliancereport.py +5 -0
- office365/sharepoint/businessdata/app_bdc_catalog.py +135 -0
- office365/sharepoint/businessdata/entityfieldvaluedictionary.py +65 -0
- office365/sharepoint/businessdata/entityidentifier.py +23 -0
- office365/sharepoint/businessdata/entityinstance.py +52 -0
- office365/sharepoint/businessdata/entityview.py +31 -0
- office365/sharepoint/businessdata/externalsubscriptionstore.py +9 -0
- office365/sharepoint/businessdata/typedescriptor.py +37 -0
- office365/sharepoint/client_context.py +40 -23
- office365/sharepoint/clientsidecomponent/hostedapps/app.py +21 -0
- office365/sharepoint/clientsidecomponent/hostedapps/manager.py +16 -0
- office365/sharepoint/comments/comment.py +84 -2
- office365/sharepoint/contentcenter/machinelearning/models/model.py +0 -23
- office365/sharepoint/contentcenter/machinelearning/publications/publication.py +0 -9
- office365/sharepoint/contentcenter/machinelearning/samples/sample.py +0 -14
- office365/sharepoint/contentcenter/machinelearning/workitems/item.py +0 -80
- office365/sharepoint/contentsolution/template_api.py +148 -0
- office365/sharepoint/contenttypes/collection.py +32 -10
- office365/sharepoint/convergence/addnoliveblobsinodcservicestateapi.py +9 -0
- office365/sharepoint/convergence/consumer_permissions.py +15 -0
- office365/sharepoint/convergence/migration_complete_state_api.py +25 -0
- office365/sharepoint/convergence/odcmetadatacleanedupapi.py +9 -0
- office365/sharepoint/documentmanagement/document_id.py +12 -0
- office365/sharepoint/entity.py +11 -2
- office365/sharepoint/entity_collection.py +2 -2
- office365/sharepoint/exceptions.py +194 -0
- office365/sharepoint/fields/builtin_field_id.py +45 -34
- office365/sharepoint/fields/builtin_field_name.py +123 -0
- office365/sharepoint/fields/coercion.py +131 -0
- office365/sharepoint/fields/collection.py +54 -37
- office365/sharepoint/fields/field.py +17 -0
- office365/sharepoint/fields/name.py +46 -2
- office365/sharepoint/files/collection.py +7 -1
- office365/sharepoint/files/file.py +54 -8
- office365/sharepoint/files/versions/version.py +38 -13
- office365/sharepoint/folders/collection.py +4 -0
- office365/sharepoint/folders/folder.py +91 -21
- office365/sharepoint/htmlpages/html_page.py +16 -0
- office365/sharepoint/htmlpages/html_page_service.py +19 -0
- office365/sharepoint/ir/migration.py +9 -0
- office365/sharepoint/listitems/caml/__init__.py +31 -0
- office365/sharepoint/listitems/caml/builder.py +129 -0
- office365/sharepoint/listitems/caml/expressions.py +424 -0
- office365/sharepoint/listitems/caml/fields.py +68 -0
- office365/sharepoint/listitems/caml/guard.py +63 -0
- office365/sharepoint/listitems/caml/query.py +71 -2
- office365/sharepoint/listitems/caml/values.py +113 -0
- office365/sharepoint/listitems/collection.py +21 -20
- office365/sharepoint/listitems/upsert.py +75 -0
- office365/sharepoint/lists/collection.py +29 -22
- office365/sharepoint/lists/exporter.py +4 -3
- office365/sharepoint/lists/list.py +397 -39
- office365/sharepoint/lists/smart_wiki_library_utility.py +58 -0
- office365/sharepoint/logger/export.py +12 -0
- office365/sharepoint/marketplace/app_metadata.py +97 -1
- office365/sharepoint/marketplace/appstatequeryjobdefinition.py +9 -0
- office365/sharepoint/marketplace/corporatecuratedgallery/carddesigns/card_designs.py +35 -0
- office365/sharepoint/marketplace/sitecollection/appcatalog/allowed_item.py +5 -0
- office365/sharepoint/marketplace/tenant/appcatalog/accessor.py +107 -30
- office365/sharepoint/microfeed/data.py +0 -6
- office365/sharepoint/microservice/manager.py +61 -0
- office365/sharepoint/migrationcenter/common/task.py +193 -4
- office365/sharepoint/migrationcenter/common/task_entity_data.py +1 -1
- office365/sharepoint/migrationcenter/deploy_status.py +13 -0
- office365/sharepoint/migrationcenter/service/properties.py +20 -0
- office365/sharepoint/migrationcenter/service/storage.py +34 -0
- office365/sharepoint/migrationcenter/spflight.py +13 -0
- office365/sharepoint/migrationcenter/urlparseresult.py +77 -0
- office365/sharepoint/multigeo/service/spmultigeoproxy.py +13 -0
- office365/sharepoint/navigation/servicerest.py +127 -0
- office365/sharepoint/oauth/authentication.py +13 -0
- office365/sharepoint/portal/theme_manager.py +120 -8
- office365/sharepoint/principal/users/user.py +10 -0
- office365/sharepoint/publishing/campaign/campaign.py +36 -1
- office365/sharepoint/publishing/feedvideopage.py +0 -7
- office365/sharepoint/publishing/pages/3d/page.py +0 -6
- office365/sharepoint/publishing/pages/page.py +0 -117
- office365/sharepoint/publishing/pages/reposts/repost.py +0 -7
- office365/sharepoint/publishing/pages/topics/topic.py +0 -7
- office365/sharepoint/publishing/subtitlefile.py +0 -40
- office365/sharepoint/quotamanagement/consumer/consumer_quota_info_api.py +14 -0
- office365/sharepoint/quotamanagement/consumer/non_quota_backfill_api.py +23 -0
- office365/sharepoint/quotamanagement/consumer/nonquotamigrationapi.py +9 -0
- office365/sharepoint/quotamanagement/consumer/quotamigrationapi.py +13 -0
- office365/sharepoint/request.py +57 -16
- office365/sharepoint/search/administration/document_crawl_log.py +24 -7
- office365/sharepoint/search/administration/providers/crawl_versions_info.py +29 -0
- office365/sharepoint/search/administration/providers/recycle_bin_info.py +23 -0
- office365/sharepoint/search/administration/providers/site_content_processing_info.py +67 -0
- office365/sharepoint/sites/site.py +976 -65
- office365/sharepoint/sites/siteprovisioninginfoprovider.py +13 -0
- office365/sharepoint/tenant/administration/datagovernance/insight_client.py +292 -23
- office365/sharepoint/tenant/administration/ddiadapter.py +97 -0
- office365/sharepoint/tenant/administration/internal/aad/collection.py +20 -0
- office365/sharepoint/tenant/administration/internal/aad/permission_grant.py +0 -19
- office365/sharepoint/tenant/administration/internal/appservice/permission_request.py +72 -1
- office365/sharepoint/tenant/administration/internal/appservice/principal.py +1 -3
- office365/sharepoint/tenant/administration/odb_license_enforcement_service.py +26 -0
- office365/sharepoint/tenant/administration/sites/properties.py +564 -6
- office365/sharepoint/tenant/administration/spdatagovernancesarrestapiclient.py +60 -1
- office365/sharepoint/tenant/insights/reports/manager.py +24 -12
- office365/sharepoint/tenant/management/office365_tenant.py +451 -2
- office365/sharepoint/tenantadministration/catalog_management_collection.py +69 -0
- office365/sharepoint/tenantadministration/catalog_management_custom_properties_collection.py +237 -0
- office365/sharepoint/tenantadministration/catalog_management_restricted_content_discovery_collection.py +56 -0
- office365/sharepoint/tenantadministration/sp_data_governance_client_exception.py +7 -0
- office365/sharepoint/thresholds.py +529 -0
- office365/sharepoint/translation/jobs/job.py +72 -2
- office365/sharepoint/translation/jobs/status.py +47 -9
- office365/sharepoint/translation/sync_translator.py +15 -2
- office365/sharepoint/utilities/move_copy_util.py +2 -1
- office365/sharepoint/viva/connections/page.py +27 -0
- office365/sharepoint/viva/home.py +38 -0
- office365/sharepoint/viva/learningmanager.py +9 -0
- office365/sharepoint/viva/vivaresources.py +60 -0
- office365/sharepoint/webhooks/subscription.py +0 -11
- office365/sharepoint/webparts/definitions/definition.py +13 -0
- office365/sharepoint/webparts/limited_manager.py +29 -7
- office365/sharepoint/webs/context_web_information.py +14 -9
- office365/sharepoint/webs/templates/__init__.py +11 -0
- office365/sharepoint/webs/templates/type.py +74 -15
- office365/sharepoint/workflow/definition.py +6 -0
- office365/sharepoint/workflow/deployment_service.py +155 -0
- office365/sharepoint/workflow/instances/service.py +33 -0
- office365/sharepoint/workflow/interopservice.py +68 -0
- office365/sharepoint/workflow/messagingservice.py +24 -0
- office365/sharepoint/workflow/servicesmanager.py +41 -2
- office365/sharepoint/workflow/subscriptions/service.py +67 -0
- office365/sharepoint/workflow/subscriptions/subscription.py +31 -0
- office365/sharepoint/workflow/task.py +425 -0
- office365_rest_python_client-3.2.0.dist-info/METADATA +605 -0
- {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/RECORD +242 -185
- office365/migration/_util.py +0 -73
- office365/sharepoint/listitems/caml/types.py +0 -70
- office365_rest_python_client-3.1.0.dist-info/METADATA +0 -475
- {office365_rest_python_client-3.1.0.dist-info → office365_rest_python_client-3.2.0.dist-info}/WHEEL +0 -0
|
@@ -22,16 +22,17 @@ from __future__ import annotations
|
|
|
22
22
|
from collections.abc import Callable
|
|
23
23
|
from typing import TYPE_CHECKING
|
|
24
24
|
|
|
25
|
-
from office365.migration._util import emit_progress
|
|
26
25
|
from office365.migration.assessment.containers import ScanContainer
|
|
27
26
|
from office365.migration.assessment.issue import AssessmentIssue
|
|
28
27
|
from office365.migration.assessment.report import AssessmentReport
|
|
29
28
|
from office365.migration.assessment.runner import ScanRunner
|
|
30
29
|
from office365.migration.assessment.scanners import AssessmentOptions
|
|
31
30
|
from office365.migration.sharepoint.adapters import is_taxonomy_validation
|
|
31
|
+
from office365.migration.sharepoint.options import SharePointAssessmentOptions
|
|
32
32
|
from office365.migration.sharepoint.registry import sharepoint_scan_pairs
|
|
33
33
|
from office365.migration.sharepoint.scanners.summary import SiteScanSummary
|
|
34
34
|
from office365.runtime.client_result import ClientResult
|
|
35
|
+
from office365.runtime.operations import emit_progress
|
|
35
36
|
from office365.sharepoint.entity import Entity
|
|
36
37
|
|
|
37
38
|
if TYPE_CHECKING:
|
|
@@ -56,7 +57,7 @@ class MigrationAssessor(Entity):
|
|
|
56
57
|
def __init__(self, web: "Web", options: AssessmentOptions | None = None) -> None:
|
|
57
58
|
super().__init__(web.context)
|
|
58
59
|
self._web = web
|
|
59
|
-
self._options = options or
|
|
60
|
+
self._options = options or SharePointAssessmentOptions()
|
|
60
61
|
|
|
61
62
|
# ── Configuration ────────────────────────────────────────────
|
|
62
63
|
|
|
@@ -104,7 +105,11 @@ class MigrationAssessor(Entity):
|
|
|
104
105
|
report = runner.report
|
|
105
106
|
|
|
106
107
|
needs_site = bool(runner.scanners(ScanContainer.SITE))
|
|
107
|
-
|
|
108
|
+
needs_site_template = any(
|
|
109
|
+
scanner.scan_name == "UnsupportedSiteTemplates" for scanner in runner.scanners(ScanContainer.SITE)
|
|
110
|
+
)
|
|
111
|
+
# item counts / last-modified feed the SITE summary and the LIST-container scans
|
|
112
|
+
needs_list_metadata = needs_site or bool(runner.scanners(ScanContainer.LIST))
|
|
108
113
|
summary = SiteScanSummary()
|
|
109
114
|
|
|
110
115
|
def _flag_failure(location: str, error: Exception) -> None:
|
|
@@ -138,9 +143,14 @@ class MigrationAssessor(Entity):
|
|
|
138
143
|
# site collection metadata (usage/storage, owner) for SITE-container scans
|
|
139
144
|
if needs_site:
|
|
140
145
|
site = self._web.context.site
|
|
146
|
+
columns = ["Id", "Url", "UsageInfo", "Owner/Title", "Owner/Email"]
|
|
147
|
+
expands = ["Owner"]
|
|
148
|
+
if needs_site_template: # the root web template feeds UnsupportedSiteTemplates
|
|
149
|
+
columns += ["RootWeb/WebTemplate", "RootWeb/Configuration"]
|
|
150
|
+
expands.append("RootWeb")
|
|
141
151
|
(
|
|
142
|
-
site.select(
|
|
143
|
-
.expand(
|
|
152
|
+
site.select(columns)
|
|
153
|
+
.expand(expands)
|
|
144
154
|
.get()
|
|
145
155
|
.on_error(lambda e: _flag_failure("web", e))
|
|
146
156
|
.after_execute(lambda site: self._on_site_loaded(site, summary, needs_site))
|
|
@@ -182,6 +192,20 @@ class MigrationAssessor(Entity):
|
|
|
182
192
|
site.root_web.associated_owner_group.users.get().on_error(lambda e: None).after_execute(
|
|
183
193
|
lambda users: self._set_site_admins(summary, users)
|
|
184
194
|
)
|
|
195
|
+
root_web = site.properties.get("RootWeb")
|
|
196
|
+
if root_web is not None:
|
|
197
|
+
summary.web_template = self._web_template(root_web)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _web_template(web: "Web") -> str | None:
|
|
201
|
+
"""The root web's ``WebTemplate`` plus its configuration id (e.g. ``STS#0``)."""
|
|
202
|
+
template = web.properties.get("WebTemplate")
|
|
203
|
+
if not template:
|
|
204
|
+
return None
|
|
205
|
+
if "#" in template:
|
|
206
|
+
return template
|
|
207
|
+
configuration = web.properties.get("Configuration")
|
|
208
|
+
return f"{template}#{configuration}" if configuration is not None else template
|
|
185
209
|
|
|
186
210
|
@staticmethod
|
|
187
211
|
def _set_site_admins(summary: SiteScanSummary, users) -> None:
|
|
@@ -247,16 +271,19 @@ class MigrationAssessor(Entity):
|
|
|
247
271
|
has_fields = bool(runner.scanners(ScanContainer.FIELDS))
|
|
248
272
|
has_items = bool(runner.scanners(ScanContainer.ITEMS, "default"))
|
|
249
273
|
has_unique_items = bool(runner.scanners(ScanContainer.ITEMS, "unique"))
|
|
274
|
+
has_list = bool(runner.scanners(ScanContainer.LIST))
|
|
250
275
|
|
|
251
276
|
def _progress(lst) -> None:
|
|
252
277
|
completed["count"] += 1
|
|
253
278
|
emit_progress(progress, done=completed["count"], total=total, stage="assessing", items=[lst])
|
|
254
279
|
|
|
255
280
|
for lst in lists:
|
|
281
|
+
location = f"{prefix}/lists/{lst.title}"
|
|
282
|
+
if has_list:
|
|
283
|
+
runner.dispatch(ScanContainer.LIST, lst, location)
|
|
256
284
|
if lst.hidden:
|
|
257
285
|
_progress(lst)
|
|
258
286
|
continue
|
|
259
|
-
location = f"{prefix}/lists/{lst.title}"
|
|
260
287
|
pending = {"count": 0}
|
|
261
288
|
|
|
262
289
|
def _scan_done(lst=lst, pending=pending) -> None:
|
|
@@ -278,9 +305,18 @@ class MigrationAssessor(Entity):
|
|
|
278
305
|
)
|
|
279
306
|
if has_items:
|
|
280
307
|
pending["count"] += 1
|
|
281
|
-
lst.items.select(
|
|
282
|
-
|
|
283
|
-
|
|
308
|
+
lst.items.select(
|
|
309
|
+
[
|
|
310
|
+
"FileRef",
|
|
311
|
+
"FileLeafRef",
|
|
312
|
+
"File/Length",
|
|
313
|
+
"File/MajorVersion",
|
|
314
|
+
"File/MinorVersion",
|
|
315
|
+
"File/CheckOutType",
|
|
316
|
+
"File/TimeCreated",
|
|
317
|
+
"File/TimeLastModified",
|
|
318
|
+
]
|
|
319
|
+
).expand(["File", "File/ModifiedBy", "File/CheckedOutByUser"]).get().on_error(_fail).after_execute(
|
|
284
320
|
lambda col, lst=lst, loc=location, done=_scan_done: (
|
|
285
321
|
self._scan_items(runner, col, loc),
|
|
286
322
|
done(lst),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""SharePoint assessment options — the SharePoint defaults for the scanners.
|
|
2
|
+
|
|
3
|
+
Seeds the product-agnostic :class:`~office365.migration.assessment.scanners.AssessmentOptions`
|
|
4
|
+
with the SharePoint thresholds (from ``office365.sharepoint.thresholds.Limits``)
|
|
5
|
+
and the ``limits`` mapping, so findings reference the authoritative limit.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from office365.migration.assessment.scanners import AssessmentOptions
|
|
14
|
+
from office365.runtime.limits import Limit
|
|
15
|
+
from office365.sharepoint.fields.builtin_field_name import SYSTEM_FIELD_NAMES
|
|
16
|
+
from office365.sharepoint.thresholds import Limits
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _limits() -> dict[str, Limit]:
|
|
20
|
+
return {
|
|
21
|
+
"max_path_length": Limits.FILE_PATH_LENGTH,
|
|
22
|
+
"max_name_length": Limits.FILE_NAME_LENGTH,
|
|
23
|
+
"large_file_bytes": Limits.MIGRATION_FILE_SIZE,
|
|
24
|
+
"large_excel_bytes": Limits.LARGE_EXCEL_FILE,
|
|
25
|
+
"long_onedrive_url": Limits.LONG_ONEDRIVE_URL,
|
|
26
|
+
"list_view_threshold": Limits.LIST_VIEW,
|
|
27
|
+
"index_threshold": Limits.INDEX_ADD_REMOVE,
|
|
28
|
+
"max_list_items": Limits.MAX_LIST_ITEMS,
|
|
29
|
+
"lookup_joins": Limits.LOOKUP_JOINS,
|
|
30
|
+
"unique_scopes": Limits.UNIQUE_SCOPES,
|
|
31
|
+
"recommended_unique_scopes": Limits.UNIQUE_SCOPES_RECOMMENDED,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class SharePointAssessmentOptions(AssessmentOptions):
|
|
37
|
+
"""The SharePoint defaults — thresholds + the ``limits`` mapping."""
|
|
38
|
+
|
|
39
|
+
max_path_length: Optional[int] = Limits.FILE_PATH_LENGTH.value
|
|
40
|
+
max_name_length: Optional[int] = Limits.FILE_NAME_LENGTH.value
|
|
41
|
+
invalid_chars: set[str] = field(default_factory=lambda: set(r'~"#%&*:<>?/\{|}'))
|
|
42
|
+
large_file_bytes: Optional[int] = Limits.MIGRATION_FILE_SIZE.value
|
|
43
|
+
large_excel_bytes: Optional[int] = Limits.LARGE_EXCEL_FILE.value
|
|
44
|
+
long_onedrive_url: Optional[int] = Limits.LONG_ONEDRIVE_URL.value
|
|
45
|
+
list_view_threshold: Optional[int] = Limits.LIST_VIEW.value
|
|
46
|
+
index_threshold: Optional[int] = Limits.INDEX_ADD_REMOVE.value
|
|
47
|
+
max_list_items: Optional[int] = Limits.MAX_LIST_ITEMS.value
|
|
48
|
+
lookup_joins: Optional[int] = Limits.LOOKUP_JOINS.value
|
|
49
|
+
unique_scopes: Optional[int] = Limits.UNIQUE_SCOPES.value
|
|
50
|
+
recommended_unique_scopes: Optional[int] = Limits.UNIQUE_SCOPES_RECOMMENDED.value
|
|
51
|
+
large_site_threshold_gb: Optional[float] = 500.0
|
|
52
|
+
strip_field_attrs: set[str] = field(default_factory=lambda: {"ColName", "RowOrdinal", "SourceID", "Version"})
|
|
53
|
+
approval_workflow_fields: set[str] = field(
|
|
54
|
+
default_factory=lambda: {"_ApprovalStatus", "_ApprovalRespondedBy", "_ApprovalAssignedTo"}
|
|
55
|
+
)
|
|
56
|
+
system_field_names: set[str] = field(default_factory=lambda: set(SYSTEM_FIELD_NAMES))
|
|
57
|
+
limits: dict[str, Limit] = field(default_factory=_limits)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Server-side migration target — build a Migration API package and ingest it.
|
|
2
|
+
|
|
3
|
+
Unlike the client-side :class:`SharePointLibraryTarget` (which uploads each file
|
|
4
|
+
over REST), this target accumulates files/folders into a Migration API package,
|
|
5
|
+
stages the blobs in Azure Storage, and submits an ingestion job — the only path
|
|
6
|
+
that preserves version history and full ACL fidelity.
|
|
7
|
+
|
|
8
|
+
Content and manifest go to **different** containers; the caller supplies their SAS
|
|
9
|
+
URIs (e.g. from ``Site.provision_migration_containers``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from office365.migration.base import MigrationItem
|
|
17
|
+
from office365.migration.package.builder import PackageBuilder
|
|
18
|
+
from office365.migration.package.staging import create_staging
|
|
19
|
+
from office365.migration.server_job import MigrationServerJob
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from office365.migration.package.builder import Package
|
|
23
|
+
|
|
24
|
+
__all__ = ["SharePointPackageTarget"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_site_url(site, site_url: str | None) -> str:
|
|
28
|
+
"""The target site URL for ``ExportSettings.xml`` (the API rejects an empty URI)."""
|
|
29
|
+
resolved = site_url or getattr(site, "url", None)
|
|
30
|
+
if not resolved and hasattr(site, "ensure_property"):
|
|
31
|
+
site.ensure_property("Url").execute_query()
|
|
32
|
+
resolved = site.url
|
|
33
|
+
if not resolved:
|
|
34
|
+
raise ValueError("site_url is required — ExportSettings.xml needs the target site URL")
|
|
35
|
+
return resolved
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SharePointPackageTarget:
|
|
39
|
+
"""A ``DataTarget`` that builds a package and submits a server-side ingestion job."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
site,
|
|
44
|
+
web_id,
|
|
45
|
+
*,
|
|
46
|
+
content_uri: str | None = None,
|
|
47
|
+
manifest_uri: str | None = None,
|
|
48
|
+
site_url: str | None = None,
|
|
49
|
+
list_title: str = "Documents",
|
|
50
|
+
list_url: str | None = None,
|
|
51
|
+
web_url: str = "/",
|
|
52
|
+
root_folder_id: str | None = None,
|
|
53
|
+
root_folder_parent_id: str | None = None,
|
|
54
|
+
source_type: str = "SharePointOnline",
|
|
55
|
+
azure_queue_report_uri: str | None = None,
|
|
56
|
+
encryption_key: bytes | None = None,
|
|
57
|
+
staging=None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._site = site
|
|
60
|
+
self._web_id = web_id
|
|
61
|
+
self._list_title = list_title
|
|
62
|
+
self._content_uri = content_uri
|
|
63
|
+
self._manifest_uri = manifest_uri
|
|
64
|
+
self._azure_queue_report_uri = azure_queue_report_uri
|
|
65
|
+
self._encryption_key = encryption_key
|
|
66
|
+
if staging is None:
|
|
67
|
+
if not (content_uri and manifest_uri):
|
|
68
|
+
raise ValueError("content_uri and manifest_uri are required unless staging is provided")
|
|
69
|
+
staging = create_staging(content_uri, manifest_uri, encryption_key=encryption_key)
|
|
70
|
+
self._staging = staging
|
|
71
|
+
self._builder = PackageBuilder(
|
|
72
|
+
_resolve_site_url(site, site_url),
|
|
73
|
+
web_id=web_id,
|
|
74
|
+
web_url=web_url,
|
|
75
|
+
list_title=list_title,
|
|
76
|
+
list_url=list_url,
|
|
77
|
+
root_folder_id=root_folder_id,
|
|
78
|
+
root_folder_parent_id=root_folder_parent_id,
|
|
79
|
+
source_type=source_type,
|
|
80
|
+
)
|
|
81
|
+
self.job_id: str | None = None
|
|
82
|
+
self._package: "Package | None" = None
|
|
83
|
+
|
|
84
|
+
def label(self) -> str:
|
|
85
|
+
return f"migration-package:{self._list_title}"
|
|
86
|
+
|
|
87
|
+
# ── DataTarget ───────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def exists(self, item: MigrationItem) -> bool:
|
|
90
|
+
# The server-side API handles overwrite/merge; nothing is checked client-side.
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def write(self, item: MigrationItem, payload: object) -> None:
|
|
94
|
+
if item.item_type == "folder":
|
|
95
|
+
self._builder.add_folder(
|
|
96
|
+
item.dest_path,
|
|
97
|
+
time_created=item.created,
|
|
98
|
+
time_last_modified=item.modified,
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
content = payload if isinstance(payload, bytes) else str(payload).encode("utf-8")
|
|
102
|
+
self._builder.add_file(
|
|
103
|
+
item.dest_path,
|
|
104
|
+
content,
|
|
105
|
+
time_created=item.created,
|
|
106
|
+
time_last_modified=item.modified,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def list_paths(self) -> list[str]:
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
def checksum(self, item: MigrationItem) -> str:
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
# ── Package + submission ─────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
def build(self) -> "Package":
|
|
118
|
+
"""Render the package from the items written so far."""
|
|
119
|
+
self._package = self._builder.build()
|
|
120
|
+
return self._package
|
|
121
|
+
|
|
122
|
+
def stage(self) -> "Package":
|
|
123
|
+
"""Build the package and stage its blobs for ingestion."""
|
|
124
|
+
package = self.build()
|
|
125
|
+
self._staging.stage(package)
|
|
126
|
+
return package
|
|
127
|
+
|
|
128
|
+
def commit(self, options=None) -> None:
|
|
129
|
+
"""Stage the package and submit the ingestion job (called by the runner)."""
|
|
130
|
+
if not (self._content_uri and self._manifest_uri):
|
|
131
|
+
raise ValueError("content_uri and manifest_uri are required to submit an ingestion job")
|
|
132
|
+
self.stage()
|
|
133
|
+
server = MigrationServerJob(self._site)
|
|
134
|
+
if self._encryption_key is not None:
|
|
135
|
+
self.job_id = server.submit_encrypted(
|
|
136
|
+
self._web_id,
|
|
137
|
+
self._content_uri,
|
|
138
|
+
self._manifest_uri,
|
|
139
|
+
self._encryption_key,
|
|
140
|
+
self._azure_queue_report_uri,
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
self.job_id = server.submit(
|
|
144
|
+
self._web_id,
|
|
145
|
+
self._content_uri,
|
|
146
|
+
self._manifest_uri,
|
|
147
|
+
self._azure_queue_report_uri,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def monitor(self, interval: float = 5, timeout: float = 1800, progress=None) -> str:
|
|
151
|
+
"""Poll the submitted job to completion (``GetMigrationJobProgress``)."""
|
|
152
|
+
if self.job_id is None:
|
|
153
|
+
raise ValueError("no job submitted yet — call commit() first")
|
|
154
|
+
return MigrationServerJob(self._site).monitor(self.job_id, interval=interval, timeout=timeout, progress=progress)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def staging(self):
|
|
158
|
+
"""The staging strategy (``FileSystemStaging`` / ``BlobStaging``)."""
|
|
159
|
+
return self._staging
|
|
160
|
+
|
|
161
|
+
def events(self) -> list[dict]:
|
|
162
|
+
"""All progress events for the submitted job (call after :meth:`monitor`)."""
|
|
163
|
+
if self.job_id is None:
|
|
164
|
+
return []
|
|
165
|
+
return MigrationServerJob(self._site).all_events(self.job_id)
|
|
166
|
+
|
|
167
|
+
def errors(self) -> list[dict]:
|
|
168
|
+
"""The ``JobError`` events for the submitted job (call after :meth:`monitor`)."""
|
|
169
|
+
if self.job_id is None:
|
|
170
|
+
return []
|
|
171
|
+
return MigrationServerJob(self._site).errors(self.job_id)
|
|
172
|
+
|
|
173
|
+
def diagnose(self) -> list[str]:
|
|
174
|
+
"""Human-readable reasons the submitted job failed (errors, events, API log).
|
|
175
|
+
|
|
176
|
+
The progress events omit the detail; the API writes it to a log in the
|
|
177
|
+
manifest container (named by the ``JobLogFileCreate`` event), which this
|
|
178
|
+
fetches — the container grants ``Read``.
|
|
179
|
+
"""
|
|
180
|
+
if self.job_id is None:
|
|
181
|
+
return []
|
|
182
|
+
events = self.events()
|
|
183
|
+
lines = [f"! {e.get('ErrorType', 'error')}: {e.get('Message', '')}".rstrip() for e in self.errors()]
|
|
184
|
+
if not lines:
|
|
185
|
+
lines = [f"· {e.get('Event')}: {e.get('Message', '')}".rstrip() for e in events]
|
|
186
|
+
lines.extend(self._read_logs(events))
|
|
187
|
+
return lines
|
|
188
|
+
|
|
189
|
+
def _read_logs(self, events: list[dict]) -> list[str]:
|
|
190
|
+
reader = getattr(self._staging, "read_manifest_blob", None)
|
|
191
|
+
if not callable(reader):
|
|
192
|
+
return []
|
|
193
|
+
names = [event.get("FileName") for event in events if event.get("FileName")]
|
|
194
|
+
if not names and self.job_id:
|
|
195
|
+
names = [f"Import-{self.job_id}-1.log"]
|
|
196
|
+
lines: list[str] = []
|
|
197
|
+
for name in names:
|
|
198
|
+
try:
|
|
199
|
+
data = reader(name)
|
|
200
|
+
text = data.decode("utf-8", "replace") if isinstance(data, bytes) else str(data)
|
|
201
|
+
except Exception as exc: # noqa: BLE001 — diagnostics are best-effort
|
|
202
|
+
lines.append(f"(log {name} unavailable: {exc})")
|
|
203
|
+
else:
|
|
204
|
+
lines.append(f"--- {name} ---")
|
|
205
|
+
lines.append(text[:4000])
|
|
206
|
+
return lines
|
|
207
|
+
|
|
208
|
+
def close(self) -> None:
|
|
209
|
+
pass
|
|
@@ -10,19 +10,41 @@ from __future__ import annotations
|
|
|
10
10
|
from office365.migration.assessment.containers import ScanContainer
|
|
11
11
|
from office365.migration.assessment.registry import ScanDefinition, scan_pairs
|
|
12
12
|
from office365.migration.assessment.scanners.base import AssessmentOptions, BaseScanner
|
|
13
|
+
from office365.migration.sharepoint.scanners.browser_file_handling import BrowserFileHandlingScanner
|
|
14
|
+
from office365.migration.sharepoint.scanners.checked_out_files import CheckedOutFilesScanner
|
|
13
15
|
from office365.migration.sharepoint.scanners.fields import FieldScanner
|
|
16
|
+
from office365.migration.sharepoint.scanners.file_versions import FileVersionsScanner
|
|
14
17
|
from office365.migration.sharepoint.scanners.files import FileScanner
|
|
18
|
+
from office365.migration.sharepoint.scanners.large_excel_files import LargeExcelFilesScanner
|
|
19
|
+
from office365.migration.sharepoint.scanners.large_lists import LargeListScanner
|
|
15
20
|
from office365.migration.sharepoint.scanners.large_sites import LargeSitesScanner
|
|
16
21
|
from office365.migration.sharepoint.scanners.locked_sites import SiteLockedScanner
|
|
22
|
+
from office365.migration.sharepoint.scanners.long_onedrive_urls import LongOneDriveUrlsScanner
|
|
23
|
+
from office365.migration.sharepoint.scanners.lookup import LookupColumnScanner
|
|
17
24
|
from office365.migration.sharepoint.scanners.paths import PathScanner
|
|
18
25
|
from office365.migration.sharepoint.scanners.permissions import PermissionScanner
|
|
26
|
+
from office365.migration.sharepoint.scanners.thicket_folders import ThicketFolderScanner
|
|
27
|
+
from office365.migration.sharepoint.scanners.unsupported_site_templates import UnsupportedSiteTemplatesScanner
|
|
19
28
|
|
|
20
29
|
SHAREPOINT_SCANS: list[ScanDefinition] = [
|
|
21
30
|
ScanDefinition(name="fields", scanner=FieldScanner, container=ScanContainer.FIELDS),
|
|
31
|
+
ScanDefinition(name="lookups", scanner=LookupColumnScanner, container=ScanContainer.FIELDS),
|
|
32
|
+
ScanDefinition(name="largeLists", scanner=LargeListScanner, container=ScanContainer.LIST),
|
|
22
33
|
ScanDefinition(name="paths", scanner=PathScanner, container=ScanContainer.ITEMS),
|
|
23
34
|
ScanDefinition(name="files", scanner=FileScanner, container=ScanContainer.ITEMS),
|
|
35
|
+
ScanDefinition(name="FileVersions", scanner=FileVersionsScanner, container=ScanContainer.ITEMS),
|
|
36
|
+
ScanDefinition(name="CheckedOutFiles", scanner=CheckedOutFilesScanner, container=ScanContainer.ITEMS),
|
|
37
|
+
ScanDefinition(name="LargeExcelFiles", scanner=LargeExcelFilesScanner, container=ScanContainer.ITEMS),
|
|
38
|
+
ScanDefinition(name="LongOneDriveUrls", scanner=LongOneDriveUrlsScanner, container=ScanContainer.ITEMS),
|
|
39
|
+
ScanDefinition(name="BrowserFileHandling", scanner=BrowserFileHandlingScanner, container=ScanContainer.ITEMS),
|
|
40
|
+
ScanDefinition(name="ThicketFolder", scanner=ThicketFolderScanner, container=ScanContainer.ITEMS),
|
|
24
41
|
ScanDefinition(name="permissions", scanner=PermissionScanner, container=ScanContainer.ITEMS),
|
|
25
42
|
ScanDefinition(name="LargeSites", scanner=LargeSitesScanner, container=ScanContainer.SITE),
|
|
43
|
+
ScanDefinition(
|
|
44
|
+
name="UnsupportedSiteTemplates",
|
|
45
|
+
scanner=UnsupportedSiteTemplatesScanner,
|
|
46
|
+
container=ScanContainer.SITE,
|
|
47
|
+
),
|
|
26
48
|
ScanDefinition(
|
|
27
49
|
name="LockedSites",
|
|
28
50
|
scanner=SiteLockedScanner,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Resolve a declarative :class:`MigrationTask` into source/target adapters.
|
|
2
|
+
|
|
3
|
+
SPMT-shaped tasks (``Add-SPMTTask``) name a source and a target site/list; this
|
|
4
|
+
turns them into the toolkit's ``DataSource``/``DataTarget`` pair against a
|
|
5
|
+
SharePoint ``ClientContext``. Client-side REST uploads are the default; set
|
|
6
|
+
``MigrationSettings.use_migration_api`` for the server-side Migration API path
|
|
7
|
+
(full fidelity: versions/ACLs).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from office365.migration.adapters.filesystem import FileSystemSource
|
|
15
|
+
from office365.migration.tasks import FILE_SHARE, SHAREPOINT, MigrationTask
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from office365.migration.settings import MigrationSettings
|
|
19
|
+
|
|
20
|
+
__all__ = ["resolve_task"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_task(task: MigrationTask, context, settings: "MigrationSettings | None" = None):
|
|
24
|
+
"""Return a ``(source, target)`` adapter pair for a task descriptor."""
|
|
25
|
+
if task.kind == FILE_SHARE:
|
|
26
|
+
return _file_share(task, context, settings)
|
|
27
|
+
if task.kind == SHAREPOINT:
|
|
28
|
+
return _sharepoint(task, context, settings)
|
|
29
|
+
raise ValueError(f"unsupported task kind {task.kind!r}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _file_share(task: MigrationTask, context, settings):
|
|
33
|
+
if not task.file_share_source:
|
|
34
|
+
raise ValueError("a file-share task requires file_share_source")
|
|
35
|
+
source = FileSystemSource(task.file_share_source)
|
|
36
|
+
library, folder = _target_folder(context, task)
|
|
37
|
+
return source, _build_target(task, context, settings, library, folder)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _sharepoint(task: MigrationTask, context, settings):
|
|
41
|
+
from office365.migration.sharepoint.adapters import SharePointLibrarySource
|
|
42
|
+
|
|
43
|
+
if task.migrate_all or not task.source_list:
|
|
44
|
+
raise NotImplementedError(
|
|
45
|
+
"SharePoint-source tasks need a source list; migrate-all / whole-site migrations are not supported yet"
|
|
46
|
+
)
|
|
47
|
+
source_list = context.web.get_list_by_title(task.source_list)
|
|
48
|
+
source = SharePointLibrarySource(source_list.root_folder)
|
|
49
|
+
library, folder = _target_folder(context, task)
|
|
50
|
+
return source, _build_target(task, context, settings, library, folder)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _target_folder(context, task: MigrationTask):
|
|
54
|
+
"""The target library (and, when set, its relative subfolder)."""
|
|
55
|
+
library = context.web.get_list_by_title(task.target_list).get().execute_query()
|
|
56
|
+
folder = library.root_folder.get().execute_query()
|
|
57
|
+
if task.target_list_relative_path:
|
|
58
|
+
url = f"{(folder.server_relative_url or '').rstrip('/')}/{task.target_list_relative_path.strip('/')}"
|
|
59
|
+
folder = context.web.get_folder_by_server_relative_url(url)
|
|
60
|
+
return library, folder
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_target(task: MigrationTask, context, settings, library, folder):
|
|
64
|
+
if settings is not None and settings.use_migration_api:
|
|
65
|
+
return _package_target(task, context, library)
|
|
66
|
+
from office365.migration.sharepoint.adapters import SharePointLibraryTarget
|
|
67
|
+
|
|
68
|
+
return SharePointLibraryTarget(folder)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _package_target(task: MigrationTask, context, library):
|
|
72
|
+
"""The server-side Migration API target (provisions containers)."""
|
|
73
|
+
from urllib.parse import urlparse
|
|
74
|
+
|
|
75
|
+
from office365.migration.sharepoint.package_target import SharePointPackageTarget
|
|
76
|
+
|
|
77
|
+
web = context.web.get().execute_query()
|
|
78
|
+
root = library.root_folder
|
|
79
|
+
root.ensure_properties(["UniqueId", "ServerRelativeUrl"]).execute_query()
|
|
80
|
+
parent = root.parent_folder.ensure_property("UniqueId")
|
|
81
|
+
parent.execute_query()
|
|
82
|
+
containers = context.site.provision_migration_containers().execute_query().value
|
|
83
|
+
return SharePointPackageTarget(
|
|
84
|
+
context.site,
|
|
85
|
+
web.id,
|
|
86
|
+
content_uri=containers.DataContainerUri,
|
|
87
|
+
manifest_uri=containers.MetadataContainerUri,
|
|
88
|
+
encryption_key=containers.EncryptionKey,
|
|
89
|
+
site_url=context.base_url,
|
|
90
|
+
list_title=library.title,
|
|
91
|
+
list_url=root.server_relative_url,
|
|
92
|
+
web_url=urlparse(context.base_url).path or "/",
|
|
93
|
+
root_folder_id=root.unique_id,
|
|
94
|
+
root_folder_parent_id=parent.unique_id,
|
|
95
|
+
source_type="FileShare" if task.kind == FILE_SHARE else "SharePointOnline",
|
|
96
|
+
)
|
|
@@ -5,23 +5,52 @@ from office365.migration.assessment.scanners.base import (
|
|
|
5
5
|
BaseScanner,
|
|
6
6
|
ScanTarget,
|
|
7
7
|
)
|
|
8
|
+
from office365.migration.sharepoint.scanners.browser_file_handling import (
|
|
9
|
+
BrowserFileHandlingRecord,
|
|
10
|
+
BrowserFileHandlingScanner,
|
|
11
|
+
)
|
|
12
|
+
from office365.migration.sharepoint.scanners.checked_out_files import (
|
|
13
|
+
CheckedOutFilesRecord,
|
|
14
|
+
CheckedOutFilesScanner,
|
|
15
|
+
)
|
|
8
16
|
from office365.migration.sharepoint.scanners.fields import FieldScanner
|
|
17
|
+
from office365.migration.sharepoint.scanners.file_versions import FileVersionsRecord, FileVersionsScanner
|
|
9
18
|
from office365.migration.sharepoint.scanners.files import FileScanner
|
|
19
|
+
from office365.migration.sharepoint.scanners.large_excel_files import LargeExcelFilesRecord, LargeExcelFilesScanner
|
|
10
20
|
from office365.migration.sharepoint.scanners.large_sites import LargeSitesScanner
|
|
11
21
|
from office365.migration.sharepoint.scanners.locked_sites import SiteLockedScanner
|
|
22
|
+
from office365.migration.sharepoint.scanners.long_onedrive_urls import LongOneDriveUrlsRecord, LongOneDriveUrlsScanner
|
|
12
23
|
from office365.migration.sharepoint.scanners.paths import PathScanner
|
|
13
24
|
from office365.migration.sharepoint.scanners.permissions import PermissionScanner
|
|
14
25
|
from office365.migration.sharepoint.scanners.summary import SiteScanSummary
|
|
26
|
+
from office365.migration.sharepoint.scanners.thicket_folders import ThicketFolderScanner
|
|
27
|
+
from office365.migration.sharepoint.scanners.unsupported_site_templates import (
|
|
28
|
+
UnsupportedSiteTemplatesRecord,
|
|
29
|
+
UnsupportedSiteTemplatesScanner,
|
|
30
|
+
)
|
|
15
31
|
|
|
16
32
|
__all__ = [
|
|
17
33
|
"AssessmentOptions",
|
|
18
34
|
"BaseScanner",
|
|
35
|
+
"BrowserFileHandlingRecord",
|
|
36
|
+
"BrowserFileHandlingScanner",
|
|
37
|
+
"CheckedOutFilesRecord",
|
|
38
|
+
"CheckedOutFilesScanner",
|
|
19
39
|
"FieldScanner",
|
|
20
40
|
"FileScanner",
|
|
41
|
+
"FileVersionsRecord",
|
|
42
|
+
"FileVersionsScanner",
|
|
43
|
+
"LargeExcelFilesRecord",
|
|
44
|
+
"LargeExcelFilesScanner",
|
|
21
45
|
"LargeSitesScanner",
|
|
46
|
+
"LongOneDriveUrlsRecord",
|
|
47
|
+
"LongOneDriveUrlsScanner",
|
|
22
48
|
"PathScanner",
|
|
23
49
|
"PermissionScanner",
|
|
24
50
|
"ScanTarget",
|
|
25
51
|
"SiteLockedScanner",
|
|
26
52
|
"SiteScanSummary",
|
|
53
|
+
"ThicketFolderScanner",
|
|
54
|
+
"UnsupportedSiteTemplatesRecord",
|
|
55
|
+
"UnsupportedSiteTemplatesScanner",
|
|
27
56
|
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Browser file handling scan — the SMAT ``BrowserFileHandling`` report.
|
|
2
|
+
|
|
3
|
+
The target enforces the "Strict" browser file handling setting, so ``.htm`` /
|
|
4
|
+
``.html`` files no longer open in the browser — users are prompted to download
|
|
5
|
+
them. This ``ITEMS``-container report lists the affected files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from office365.migration.assessment.report import AssessmentReport
|
|
14
|
+
from office365.migration.assessment.scanners.base import BaseScanner, ScanTarget
|
|
15
|
+
from office365.migration.sharepoint.scanners.smat import SiteScanRecord, file_extension, site_url
|
|
16
|
+
|
|
17
|
+
#: Extensions affected by the Strict browser file handling change.
|
|
18
|
+
HTML_EXTENSIONS = frozenset({".htm", ".html"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BrowserFileHandlingRecord(SiteScanRecord):
|
|
23
|
+
"""One row of the SMAT ``BrowserFileHandling-detail`` report."""
|
|
24
|
+
|
|
25
|
+
File: str | None = None
|
|
26
|
+
TimeCreated: datetime | None = None
|
|
27
|
+
TimeModified: datetime | None = None
|
|
28
|
+
ModifiedBy: str | None = None
|
|
29
|
+
ScanID: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BrowserFileHandlingScanner(BaseScanner[BrowserFileHandlingRecord]):
|
|
33
|
+
"""Reports ``.htm``/``.html`` files affected by Strict browser handling."""
|
|
34
|
+
|
|
35
|
+
category = "file"
|
|
36
|
+
scan_name = "BrowserFileHandling"
|
|
37
|
+
record_type = BrowserFileHandlingRecord
|
|
38
|
+
|
|
39
|
+
def run(self, target: ScanTarget, report: AssessmentReport) -> None:
|
|
40
|
+
location = site_url(target.location)
|
|
41
|
+
for item in target.entity:
|
|
42
|
+
if file_extension(item) not in HTML_EXTENSIONS:
|
|
43
|
+
continue
|
|
44
|
+
file = getattr(item, "file", None)
|
|
45
|
+
modified_by = file.modified_by if file is not None else None
|
|
46
|
+
self.records.append(
|
|
47
|
+
BrowserFileHandlingRecord(
|
|
48
|
+
SiteURL=location,
|
|
49
|
+
File=item.properties.get("FileRef", ""),
|
|
50
|
+
TimeCreated=file.time_created if file is not None else None,
|
|
51
|
+
TimeModified=file.time_last_modified if file is not None else None,
|
|
52
|
+
ModifiedBy=(modified_by.login_name or modified_by.title) if modified_by is not None else None,
|
|
53
|
+
ScanID=report.scan_id or None,
|
|
54
|
+
)
|
|
55
|
+
)
|