def get_bundle_file_data(bundle_uuid, path, use_draft=None): """ Read all the data in the given bundle file and return it as a binary string. Do not use this for large files! """ if use_draft: try: draft_model = _draft_queryset().get(bundle__uuid=bundle_uuid, name=use_draft) except models.Draft.DoesNotExist: pass else: draft_repo = DraftRepo(SnapshotRepo()) staged_draft = draft_model.staged_draft with draft_repo.open(staged_draft, path) as file: return file.read() bundle_version_model = _get_bundle_version_model(bundle_uuid, 0) snapshot_repo = SnapshotRepo() snapshot = bundle_version_model.snapshot() with snapshot_repo.open(snapshot, path) as file: return file.read()
def destroy(self, request, uuid): # pylint: disable=arguments-differ """ This removes any files that were staged along with the database entry. """ draft_repo = DraftRepo(SnapshotRepo()) draft_repo.delete(uuid) return super().destroy(request, uuid)
def to_representation(self, value): """StagedDraft JSON serialization.""" staged_draft = value draft_repo = DraftRepo(SnapshotRepo()) if staged_draft.base_snapshot is None: base_snapshot_repr = None else: base_snapshot_repr = staged_draft.base_snapshot.hash_digest.hex( ) basic_info = { 'base_snapshot': base_snapshot_repr, 'created_at': staged_draft.created_at, 'updated_at': staged_draft.updated_at, } basic_info['files'] = { path: { "url": draft_repo.url(staged_draft, path), "size": file_info.size, "hash_digest": file_info.hash_digest.hex(), "modified": path in staged_draft.files_to_overwrite } for path, file_info in staged_draft.files.items() } return basic_info
def to_representation(self, value): """Snapshot JSON serialization.""" snapshot = value snapshot_repo = SnapshotRepo() info = { 'hash_digest': snapshot.hash_digest.hex(), 'created_at': snapshot.created_at, } info['files'] = { path: { "url": self._expand_url(snapshot_repo.url(snapshot, path)), "size": file_info.size, "hash_digest": file_info.hash_digest.hex(), } for path, file_info in snapshot.files.items() } info['links'] = { link.name: { "direct": self._serialized_dep(link.direct_dependency), "indirect": [ self._serialized_dep(dep) for dep in link.indirect_dependencies ] } for link in snapshot.links } return info
def _bundle_version_data_from_model(bundle_version_model): """ Create and return BundleVersionData from bundle version model. """ snapshot = bundle_version_model.snapshot() snapshot_repo = SnapshotRepo() return BundleVersionData( bundle_uuid=bundle_version_model.bundle.uuid, version=bundle_version_model.version_num, change_description=bundle_version_model.change_description, created_at=snapshot.created_at, files={ path: BundleFileData( path=path, url=_build_absolute_uri(snapshot_repo.url(snapshot, path)), size=file_info.size, hash_digest=file_info.hash_digest.hex(), ) for path, file_info in snapshot.files.items() }, links={ link.name: BundleLinkData( name=link.name, direct=link.direct_dependency, indirect=link.indirect_dependencies, ) for link in snapshot.links }, )
def _draft_data_from_model(draft_model): """ Create and return DraftData from draft model. """ draft_repo = DraftRepo(SnapshotRepo()) staged_draft = draft_model.staged_draft return DraftData( uuid=draft_model.uuid, bundle_uuid=draft_model.bundle.uuid, name=draft_model.name, created_at=draft_model.staged_draft.created_at, updated_at=draft_model.staged_draft.updated_at, files={ path: DraftFileData( path=path, size=file_info.size, url=_build_absolute_uri(draft_repo.url(staged_draft, path)), hash_digest=file_info.hash_digest, modified=path in draft_model.staged_draft.files_to_overwrite, ) for path, file_info in staged_draft.files.items() }, links={ link.name: DraftLinkData( name=link.name, direct=link.direct_dependency, indirect=link.indirect_dependencies, modified=link.name in staged_draft.links_to_overwrite.modified_set, ) for link in staged_draft.composed_links() } )
def set_draft_link(draft_uuid, link_name, bundle_uuid, version): """ Create or replace the link with the given name in the specified draft so that it points to the specified bundle version. To delete a link, pass bundle_uuid=None, version=None. If you don't know the draft's UUID, look it up using get_or_create_bundle_draft() Does not return anything. """ data = { 'links': { link_name: {"bundle_uuid": str(bundle_uuid), "version": version} if bundle_uuid is not None else None, }, } serializer = DraftFileUpdateSerializer(data=data) serializer.is_valid(raise_exception=True) files_to_write = serializer.validated_data['files'] dependencies_to_write = serializer.validated_data['links'] draft_repo = DraftRepo(SnapshotRepo()) try: draft_repo.update(draft_uuid, files_to_write, dependencies_to_write) except LinkCycleError as exc: raise serializers.ValidationError("Link cycle detected: Cannot create draft.") from exc
def write_draft_file(draft_uuid, path, contents): """ Create or overwrite the file at 'path' in the specified draft with the given contents. To delete a file, pass contents=None. If you don't know the draft's UUID, look it up using get_or_create_bundle_draft() Does not return anything. """ data = { 'files': { path: _encode_str_for_draft(contents) if contents is not None else None, }, } serializer = DraftFileUpdateSerializer(data=data) serializer.is_valid(raise_exception=True) files_to_write = serializer.validated_data['files'] dependencies_to_write = serializer.validated_data['links'] draft_repo = DraftRepo(SnapshotRepo()) try: draft_repo.update(draft_uuid, files_to_write, dependencies_to_write) except LinkCycleError as exc: raise serializers.ValidationError("Link cycle detected: Cannot create draft.") from exc
def delete_draft(draft_uuid): """ Delete the specified draft, removing any staged changes/files/deletes. Does not return any value. """ draft_model = _get_draft_model(draft_uuid) draft_repo = DraftRepo(SnapshotRepo()) draft_repo.delete(draft_uuid) draft_model.delete()
def commit_draft(draft_uuid): """ Commit all of the pending changes in the draft, creating a new version of the associated bundle. Does not return any value. """ draft_repo = DraftRepo(SnapshotRepo()) staged_draft = draft_repo.get(draft_uuid) if not staged_draft.files_to_overwrite and not staged_draft.links_to_overwrite: raise DraftHasNoChangesToCommit("Draft {} does not have any changes to commit.".format(draft_uuid)) new_snapshot, _updated_draft = draft_repo.commit(staged_draft) models.BundleVersion.create_new_version( new_snapshot.bundle_uuid, new_snapshot.hash_digest )
def commit(self, request, uuid): """ Commit the Draft and create a new BundleVersion that points to it. In the future, we may want to separate these two steps so that we can create multiple BundleVersions at once in a single transaction, however given our modeling conventions of a course being in a single Bundle, that's not something that we need to implement immediately. We currently return a summary of the things that were created, however we may need to rethink this interface if the commit process is going to take so long as to require async processing. TODO: Test with large Bundles. """ draft_repo = DraftRepo(SnapshotRepo()) staged_draft = draft_repo.get(uuid) # Is this the appropriate response when trying to commit a Draft with # no changes? if not staged_draft.files_to_overwrite and not staged_draft.links_to_overwrite: raise serializers.ValidationError( "Draft has no changes to commit.") new_snapshot, _updated_draft = draft_repo.commit(staged_draft) new_bv = BundleVersion.create_new_version(new_snapshot.bundle_uuid, new_snapshot.hash_digest) # This is a placeholder response. May need to revisit after trying # some large commits. result = { 'bundle_version': reverse( 'api:v1:bundleversion-detail', args=[new_snapshot.bundle_uuid, new_bv.version_num], request=request, ), 'updated_draft': reverse( 'api:v1:draft-detail', args=[uuid], request=request, ) } return Response(result, status=status.HTTP_201_CREATED)
def to_representation(self, value): """Snapshot JSON serialization.""" snapshot = value snapshot_repo = SnapshotRepo() basic_info = { 'hash_digest': snapshot.hash_digest.hex(), 'created_at': snapshot.created_at, } basic_info['files'] = { path: { "url": snapshot_repo.url(snapshot, path), "size": file_info.size, "hash_digest": file_info.hash_digest.hex(), } for path, file_info in snapshot.files.items() } return basic_info
def partial_update(self, request, uuid): """ Create, update, and delete files in a Draft. The data payload in the request should be a JSON dictionary with file paths as keys and base64 encoded payloads as values. A null value means that path should be deleted. PATCH is a bit unusual for this resource in that the only thing you can patch are file data. You cannot change the draft name or the Bundle it belongs to, and the only way to update the `base_snapshot` is to commit the Draft. There is intentionally no PUT support for file data, for a few reasons: 1. We can't guarantee the semantics of a PUT against a concurrent PATCH, particularly for large numbers of files. Our files are in an object store that do not support multi-file transactions. We can't really even guarantee it for multiple concurrent PATCHes -- there's a possibility of a race condition there. 2. Bundles can become very large, and a PUT might become prohibitively large. Having everything as a PATCH lets us set somewhat sane per-PATCH request limits and let the client handle the case where we need to do multiple requests to make the necessary changes. 3. It's just simpler to have only one way to update the files. """ serializer = DraftFileUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) files_to_write = serializer.validated_data['files'] dependencies_to_write = serializer.validated_data['links'] draft_repo = DraftRepo(SnapshotRepo()) try: draft_repo.update(uuid, files_to_write, dependencies_to_write) except LinkCycleError: raise serializers.ValidationError( "Link cycle detected: Cannot create draft.") return Response(status=status.HTTP_204_NO_CONTENT)
def to_representation(self, value): """StagedDraft JSON serialization.""" staged_draft = value draft_repo = DraftRepo(SnapshotRepo()) if staged_draft.base_snapshot is None: base_snapshot_repr = None else: base_snapshot_repr = staged_draft.base_snapshot.hash_digest.hex( ) basic_info = { 'base_snapshot': base_snapshot_repr, 'created_at': staged_draft.created_at, 'updated_at': staged_draft.updated_at, } basic_info['files'] = { path: { "url": self._expand_url(draft_repo.url(staged_draft, path)), "size": file_info.size, "hash_digest": file_info.hash_digest.hex(), "modified": path in staged_draft.files_to_overwrite } for path, file_info in staged_draft.composed_files().items() } basic_info['links'] = { link.name: { "direct": self._serialized_dep(link.direct_dependency), "indirect": [ self._serialized_dep(dep) for dep in link.indirect_dependencies ], "modified": link.name in staged_draft.links_to_overwrite.modified_set } for link in staged_draft.composed_links() if link.direct_dependency } return basic_info