def deposit_state(request, deposit): """ Deposit state endpoint: return status of this deposit Example GET of state: curl -v http://localhost:8000/api/v1/location/96606387-cc70-4b09-b422-a7220606488d/sword/state/ """ if isinstance(deposit, basestring): try: deposit = models.Package.objects.get(uuid=deposit) except models.Package.DoesNotExist: return helpers.sword_error_response( request, 404, 'Deposit location {uuid} does not exist.'.format(uuid=deposit)) if request.method == 'GET': status = helpers.deposit_downloading_status(deposit) state_term = status state_description = 'Deposit initiation: ' + status # if deposit hasn't been finalized and last finalization attempt # failed, note failed finalization if deposit.misc_attributes.get( 'finalization_attempt_failed', None) and deposit.misc_attributes.get( 'deposit_completion_time', None) is None: state_description += ' (last finalization attempt failed)' # get status of download tasks, if any tasks = helpers.deposit_download_tasks(deposit) # create atom representation of download tasks entries = [] for task in tasks: task_files = models.PackageDownloadTaskFile.objects.filter( task=task) for task_file in task_files: entries.append({ 'title': task_file.filename, 'url': task_file.url, 'summary': task_file.downloading_status() }) response = HttpResponse( render_to_string('locations/api/sword/state.xml', locals())) response['Content-Type'] = 'application/atom+xml;type=feed' return response else: return helpers.sword_error_response( request, 405, 'This endpoint only responds to the GET HTTP method.')
def _handle_upload_request_with_potential_md5_checksum(request, file_path, success_status_code): """ Write the HTTP request body to a file and, if the HTTP Content-MD5 header is set, make sure the destination file has the expected checkcum Returns a response (with a specified HTTP status code) or an error response if a checksum has been provided but the destination file's is different """ temp_filepath = helpers.write_request_body_to_temp_file(request) if 'HTTP_CONTENT_MD5' in request.META: md5sum = helpers.get_file_md5_checksum(temp_filepath) if request.META['HTTP_CONTENT_MD5'] != md5sum: os.remove(temp_filepath) return helpers.sword_error_response( request, 400, 'MD5 checksum of uploaded file ({uploaded_md5sum}) does not match checksum provided in header ({header_md5sum}).' .format(uploaded_md5sum=md5sum, header_md5sum=request.META['HTTP_CONTENT_MD5'])) else: shutil.copyfile(temp_filepath, file_path) os.remove(temp_filepath) return HttpResponse(status=success_status_code) else: shutil.copyfile(temp_filepath, file_path) os.remove(temp_filepath) return HttpResponse(status=success_status_code)
def _handle_adding_to_or_replacing_file_in_deposit(request, deposit, replace_file=False): """ Parse a destination filename from an HTTP Content-Disposition header and either add or replace it Returns a response with an HTTP status code indicating whether creation (201) or updating (204) has occurred or an error response """ if 'HTTP_CONTENT_DISPOSITION' in request.META: filename = helpers.parse_filename_from_content_disposition( request.META['HTTP_CONTENT_DISPOSITION']) if filename != '': file_path = os.path.join(deposit.full_path, filename) if replace_file: # if doing a file replace, the file being replaced must exist if os.path.exists(file_path): return _handle_upload_request_with_potential_md5_checksum( request, file_path, 204) else: return helpers.sword_error_response( request, 400, 'File does not exist.') else: # if adding a file, the file must not already exist if os.path.exists(file_path): return helpers.sword_error_response( request, 400, 'File already exists.') else: return _handle_upload_request_with_potential_md5_checksum( request, file_path, 201) else: return helpers.sword_error_response( request, 400, 'No filename found in Content-disposition header.') else: return helpers.sword_error_response( request, 400, 'Content-disposition must be set in request header.')
def _finalize_or_mark_for_finalization(request, deposit): """ If a request specifies the deposit should be finalized, synchronously finalize or, if downloading is incomplete, mark for finalization. Returns deposit receipt response or error response """ if 'HTTP_IN_PROGRESS' in request.META and request.META[ 'HTTP_IN_PROGRESS'] == 'false': if helpers.deposit_downloading_status( deposit) == models.PackageDownloadTask.COMPLETE: helpers.spawn_finalization(deposit.uuid) return _deposit_receipt_response(request, deposit, 200) else: return helpers.sword_error_response( request, 400, 'Downloading not yet complete or errors were encountered.') else: return helpers.sword_error_response( request, 400, 'The In-Progress header must be set to false when starting deposit processing.' )
def collection(request, location): """ Collection endpoint: accepts deposits, and returns current deposits. Example GET of collection deposit list: curl -v http://localhost:8000/api/v1/location/96606387-cc70-4b09-b422-a7220606488d/sword/collection/ Example POST creation of deposit, allowing asynchronous downloading of object content URLs: curl -v -H "In-Progress: true" --data-binary @mets.xml --request POST http://localhost:8000/api/v1/location/96606387-cc70-4b09-b422-a7220606488d/sword/collection/ Example POST creation of deposit, finalizing the deposit and auto-approving it: curl -v -H "In-Progress: false" --data-binary @mets.xml --request POST http://localhost:8000/api/v1/location/c0bee7c8-3e9b-41e3-8600-ee9b2c475da2/sword/collection/ Example POST creation of deposit from another location: curl -v -H "In-Progress: true" --request POST http://localhost:8000/api/v1/location/96606387-cc70-4b09-b422-a7220606488d/sword/collection/?source_location=aab142a9-018f-4452-8f93-67c1bf7fd486&relative_path_to_files=archivematica-sampledata/SampleTransfers/Images """ if isinstance(location, basestring): try: location = models.Location.active.get(uuid=location) except models.Location.DoesNotExist: return helpers.sword_error_response( request, 404, 'Collection {uuid} does not exist.'.format(uuid=location)) if request.method == 'GET': # return list of deposits as ATOM feed col_iri = request.build_absolute_uri( reverse('sword_collection', kwargs={ 'api_name': 'v1', 'resource_name': 'location', 'uuid': location.uuid })) feed = {'title': 'Deposits', 'url': col_iri} entries = [] for deposit in helpers.deposit_list(location.uuid): edit_iri = request.build_absolute_uri( reverse('sword_deposit', kwargs={ 'api_name': 'v1', 'resource_name': 'file', 'uuid': deposit.uuid })) entries.append({ 'title': deposit.description or 'Deposit ' + deposit.uuid, 'url': edit_iri, }) # feed and entries passed via locals() to the template collection_xml = render_to_string('locations/api/sword/collection.xml', locals()) response = HttpResponse(collection_xml) response['Content-Type'] = 'application/atom+xml;type=feed' return response elif request.method == 'POST': # has the In-Progress header been set? if 'HTTP_IN_PROGRESS' in request.META: # process creation request, if criteria met source_location = request.GET.get('source_location', '') relative_path_to_files = request.GET.get('relative_path_to_files', '') if request.body != '': try: temp_filepath = helpers.write_request_body_to_temp_file( request) # parse name and content URLs out of XML try: mets_data = _parse_name_and_content_urls_from_mets_file( temp_filepath) except etree.XMLSyntaxError as e: os.unlink(temp_filepath) mets_data = None if mets_data != None: if mets_data['deposit_name'] == None: return helpers.sword_error_response( request, 400, 'No deposit name found in XML.') if not os.path.isdir(location.full_path): return helpers.sword_error_response( request, 500, 'Collection path (%s) does not exist: contact an administrator.' % (location.full_path)) # TODO: should get this from author header or provided XML metadata sourceofacquisition = request.META[ 'HTTP_ON_BEHALF_OF'] if 'HTTP_ON_BEHALF_OF' in request.META else None deposit = _create_deposit_directory_and_db_entry( location=location, deposit_name=mets_data['deposit_name'], sourceofacquisition=sourceofacquisition) if deposit is None: return helpers.sword_error_response( request, 500, 'Could not create deposit: contact an administrator.' ) # move METS file to submission documentation directory object_id = mets_data.get('object_id', 'fedora') helpers.store_mets_data(temp_filepath, deposit, object_id) _spawn_batch_download_and_flag_finalization_if_requested( deposit, request, mets_data) if request.META['HTTP_IN_PROGRESS'] == 'true': return _deposit_receipt_response( request, deposit, 201) else: return _deposit_receipt_response( request, deposit, 200) else: return helpers.sword_error_response( request, 412, 'Error parsing XML') except Exception as e: return helpers.sword_error_response( request, 400, traceback.format_exc()) elif source_location or relative_path_to_files: if not source_location or not relative_path_to_files: if not source_location: return helpers.sword_error_response( request, 400, 'relative_path_to_files is set, but source_location is not.' ) else: return helpers.sword_error_response( request, 400, 'source_location is set, but relative_path_to_files is not.' ) else: result = deposit_from_location_relative_path( source_location, relative_path_to_files, location) if result.get('error', False): return helpers.sword_error_response( request, 500, result['message']) else: return _deposit_receipt_response( request, result['deposit_uuid'], 200) else: return helpers.sword_error_response( request, 412, 'A request body must be sent when creating a deposit.') else: return helpers.sword_error_response( request, 412, 'The In-Progress header must be set to either true or false when creating a deposit.' ) else: return helpers.sword_error_response( request, 405, 'This endpoint only responds to the GET and POST HTTP methods.')
def deposit_media(request, deposit): """ Deposit media endpoint: list, create, update or delete files Example GET of files list: curl -v http://127.0.0.1:8000/api/v1/file/149cc29d-6472-4bcf-bee8-f8223bf60580/sword/media/ Example PUT of file: curl -v -H "Content-Disposition: attachment; filename=joke.jpg" --data-binary "@joke.jpg" --request PUT http://127.0.0.1:8000/api/v1/file/9c8b4ac0-0407-4360-a10d-af6c62a48b69/sword/media/ Example POST of file: curl -v -H "Content-Disposition: attachment; filename=joke.jpg" --data-binary "@joke.jpg" --request POST http://127.0.0.1:8000/api/v1/file/9c8b4ac0-0407-4360-a10d-af6c62a48b69/sword/media/ Example POST of METS with file info: curl -v -H "Packaging: METS" -H "In-Progress: true" --data-binary @mets.xml --request POST http://127.0.0.1:8000/api/v1/file/9c8b4ac0-0407-4360-a10d-af6c62a48b69/sword/media/ Example DELETE of all files: curl -v -XDELETE http://127.0.0.1:8000/api/v1/file/9c8b4ac0-0407-4360-a10d-af6c62a48b69/sword/media/ Example DELETE of file: curl -v -XDELETE http://127.0.0.1:8000/api/v1/file/9c8b4ac0-0407-4360-a10d-af6c62a48b69/sword/media/?filename=joke.jpg """ if isinstance(deposit, basestring): try: deposit = models.Package.objects.get(uuid=deposit) except models.Package.DoesNotExist: return helpers.sword_error_response( request, 404, 'Deposit location {uuid} does not exist.'.format(uuid=deposit)) if deposit.has_been_submitted_for_processing(): return helpers.sword_error_response( request, 400, 'This deposit has already been submitted for processing.') if request.method == 'GET': # TODO should this be returned in SWORD XML? return HttpResponse(str(os.listdir(deposit.full_path))) elif request.method == 'PUT': # replace a file in the deposit return _handle_adding_to_or_replacing_file_in_deposit( request, deposit, replace_file=True) elif request.method == 'POST': # Allow async batch upload via METS XML body content if 'HTTP_PACKAGING' in request.META and request.META[ 'HTTP_PACKAGING'] == 'METS': # If METS XML has been sent to indicate a list of files needing downloading, handle it if request.body != '': temp_filepath = helpers.write_request_body_to_temp_file( request) try: mets_data = _parse_name_and_content_urls_from_mets_file( temp_filepath) except etree.XMLSyntaxError as e: os.unlink(temp_filepath) mets_data = None if mets_data != None: # move METS file to submission documentation directory object_id = mets_data.get('object_id', 'fedora') helpers.store_mets_data(temp_filepath, deposit, object_id) _spawn_batch_download_and_flag_finalization_if_requested( deposit, request, mets_data) return _deposit_receipt_response(request, deposit, 201) else: return helpers.sword_error_response( request, 412, 'Error parsing XML.') else: return helpers.sword_error_response( request, 400, 'No METS body content sent.') else: # add a file to the deposit return _handle_adding_to_or_replacing_file_in_deposit( request, deposit) elif request.method == 'DELETE': filename = request.GET.get('filename', '') if filename != '': # Delete PackageDownloadTaskFile for this filename models.PackageDownloadTaskFile.objects.filter( task__package=deposit).filter(filename=filename).delete() # Delete empty PackageDownloadTasks for this deposit models.PackageDownloadTask.objects.filter(package=deposit).filter( download_file_set=None).delete() # Delete file file_path = os.path.join(deposit.full_path, filename) if os.path.exists(file_path): os.remove(file_path) return HttpResponse(status=204) # No content else: return helpers.sword_error_response( request, 404, 'The path to this file (%s) does not exist.' % (file_path)) else: # Delete all PackageDownloadTaskFile and PackageDownloadTask for this deposit models.PackageDownloadTaskFile.objects.filter( task__package=deposit).delete() models.PackageDownloadTask.objects.filter(package=deposit).delete() # Delete all files for filename in os.listdir(deposit.full_path): filepath = os.path.join(deposit.full_path, filename) if os.path.isfile(filepath): os.remove(filepath) elif os.path.isdir(filepath): shutil.rmtree(filepath) return HttpResponse(status=204) # No content else: return helpers.sword_error_response( request, 405, 'This endpoint only responds to the GET, POST, PUT, and DELETE HTTP methods.' )
def deposit_edit(request, deposit): """ Deposit endpoint: list info, accept new files, finalize or delete deposit. Example POST adding files to the deposit: curl -v -H "In-Progress: true" --data-binary @mets.xml --request POST http://127.0.0.1:8000/api/v1/file/149cc29d-6472-4bcf-bee8-f8223bf60580/sword/ Example POST finalization of deposit: curl -v -H "In-Progress: false" --request POST http://127.0.0.1:8000/api/v1/file/149cc29d-6472-4bcf-bee8-f8223bf60580/sword/ Example DELETE of deposit: curl -v -XDELETE http://127.0.0.1:8000/api/v1/file/149cc29d-6472-4bcf-bee8-f8223bf60580/sword/ """ if isinstance(deposit, basestring): try: deposit = models.Package.objects.get(uuid=deposit) except models.Package.DoesNotExist: return helpers.sword_error_response( request, 404, 'Deposit location {uuid} does not exist.'.format(uuid=deposit)) if deposit.has_been_submitted_for_processing(): return helpers.sword_error_response( request, 400, 'This deposit has already been submitted for processing.') if request.method == 'GET': edit_iri = request.build_absolute_uri( reverse('sword_deposit', kwargs={ 'api_name': 'v1', 'resource_name': 'file', 'uuid': deposit.uuid })) entry = {'title': deposit.description, 'url': edit_iri} response = HttpResponse( render_to_string('locations/api/sword/entry.xml', locals())) response['Content-Type'] = 'application/atom+xml' return response elif request.method == 'POST': # If METS XML has been sent to indicate a list of files needing downloading, handle it if request.body != '': temp_filepath = helpers.write_request_body_to_temp_file(request) try: mets_data = _parse_name_and_content_urls_from_mets_file( temp_filepath) except etree.XMLSyntaxError as e: os.unlink(temp_filepath) mets_data = None if mets_data is not None: # move METS file to submission documentation directory object_id = mets_data.get('object_id', 'fedora') helpers.store_mets_data(temp_filepath, deposit, object_id) _spawn_batch_download_and_flag_finalization_if_requested( deposit, request, mets_data) return _deposit_receipt_response(request, deposit, 200) else: return helpers.sword_error_response(request, 412, 'Error parsing XML.') else: # Attempt to finalize (if requested), otherwise just return deposit receipt if 'HTTP_IN_PROGRESS' in request.META and request.META[ 'HTTP_IN_PROGRESS'] == 'false': return _finalize_or_mark_for_finalization(request, deposit) else: return _deposit_receipt_response(request, deposit, 200) elif request.method == 'PUT': # TODO: implement update deposit return HttpResponse(status=204) # No content elif request.method == 'DELETE': # Delete files shutil.rmtree(deposit.full_path) # Delete all PackageDownloadTaskFile and PackageDownloadTask for this deposit models.PackageDownloadTaskFile.objects.filter( task__package=deposit).delete() models.PackageDownloadTask.objects.filter(package=deposit).delete() # TODO should this actually delete the Package entry? deposit.status = models.Package.DELETED deposit.save() return HttpResponse(status=204) # No content else: return helpers.sword_error_response( request, 405, 'This endpoint only responds to the GET, POST, PUT, and DELETE HTTP methods.' )