Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F98273998
restful.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sat, Jan 11, 16:35
Size
18 KB
Mime Type
text/x-python
Expires
Mon, Jan 13, 16:35 (2 d)
Engine
blob
Format
Raw Data
Handle
23547902
Attached To
R3600 invenio-infoscience
restful.py
View Options
# -*- coding: utf-8 -*-
##
## This file is part of Invenio.
## Copyright (C) 2013, 2014 CERN.
##
## Invenio is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation; either version 2 of the
## License, or (at your option) any later version.
##
## Invenio is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
## General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Invenio; if not, write to the Free Software Foundation, Inc.,
## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
"""Deposit REST API."""
from
flask
import
request
from
flask.ext.restful
import
Resource
,
abort
,
reqparse
from
flask.ext.login
import
current_user
from
functools
import
wraps
from
werkzeug.utils
import
secure_filename
from
invenio.ext.restful
import
require_api_auth
,
error_codes
,
\
require_oauth_scopes
,
require_header
from
invenio.modules.deposit.models
import
Deposition
,
\
DepositionFile
,
InvalidDepositionType
,
DepositionDoesNotExists
,
\
DraftDoesNotExists
,
FormDoesNotExists
,
DepositionNotDeletable
,
\
InvalidApiAction
,
FilenameAlreadyExists
,
\
FileDoesNotExists
,
ForbiddenAction
,
DepositionError
from
invenio.modules.deposit.storage
import
\
DepositionStorage
,
UploadError
from
cerberus
import
Validator
class
APIValidator
(
Validator
):
"""Add new datatype 'raw', that accepts anything."""
def
_validate_type_any
(
self
,
field
,
value
):
pass
# Request parser
list_parser
=
reqparse
.
RequestParser
()
list_parser
.
add_argument
(
'state'
,
type
=
str
)
list_parser
.
add_argument
(
'submitted'
,
type
=
bool
)
list_parser
.
add_argument
(
'type'
,
type
=
str
)
draft_data_schema
=
dict
(
metadata
=
dict
(
type
=
"dict"
),
completed
=
dict
(
type
=
"boolean"
),
)
draft_data_extended_schema
=
draft_data_schema
.
copy
()
draft_data_extended_schema
[
'type'
]
=
dict
(
type
=
"string"
)
draft_data_extended_schema
[
'draft_id'
]
=
dict
(
type
=
"string"
)
file_schema
=
dict
(
filename
=
dict
(
type
=
"string"
,
minlength
=
1
,
maxlength
=
255
),
)
file_schema_list
=
dict
(
id
=
dict
(
type
=
"string"
),
)
#
# Decorators
#
def
error_handler
(
f
):
"""Decorator to handle deposition exceptions."""
@wraps
(
f
)
def
inner
(
*
args
,
**
kwargs
):
try
:
return
f
(
*
args
,
**
kwargs
)
except
DepositionDoesNotExists
:
abort
(
404
,
message
=
"Deposition does not exist"
,
status
=
404
)
except
DraftDoesNotExists
:
abort
(
404
,
message
=
"Draft does not exist"
,
status
=
404
)
except
InvalidApiAction
:
abort
(
404
,
message
=
"Action does not exist"
,
status
=
404
)
except
DepositionNotDeletable
:
abort
(
403
,
message
=
"Deposition is not deletable"
,
status
=
403
)
except
ForbiddenAction
:
abort
(
403
,
message
=
"Forbidden"
,
status
=
403
)
except
InvalidDepositionType
:
abort
(
400
,
message
=
"Invalid deposition type"
,
status
=
400
)
except
FormDoesNotExists
:
abort
(
400
,
message
=
"Form does not exist"
,
status
=
400
)
except
FileDoesNotExists
:
abort
(
400
,
message
=
"File does not exist"
,
status
=
400
)
except
FilenameAlreadyExists
:
abort
(
400
,
message
=
"Filename already exist"
,
status
=
400
)
except
UploadError
:
abort
(
400
)
except
DepositionError
as
e
:
if
len
(
e
.
args
)
>=
1
:
abort
(
400
,
message
=
e
.
args
[
0
],
status
=
400
)
else
:
abort
(
500
,
message
=
"Internal server error"
,
status
=
500
)
return
inner
def
api_request_globals
(
f
):
"""Set a variable in request to allow identification of API requests."""
@wraps
(
f
)
def
inner
(
*
args
,
**
kwargs
):
request
.
is_api_request
=
True
return
f
(
*
args
,
**
kwargs
)
return
inner
def
filter_draft_errors
(
result
):
"""Extract error messages from a draft.process() result dictionary."""
error_messages
=
[]
for
field
,
msgs
in
result
.
get
(
'messages'
,
{})
.
items
():
if
msgs
.
get
(
'state'
,
None
)
==
'error'
:
for
m
in
msgs
[
'messages'
]:
error_messages
.
append
(
dict
(
field
=
field
,
message
=
m
,
code
=
error_codes
[
'validation_error'
],
))
return
error_messages
def
filter_validation_errors
(
errors
):
"""Extract error messages from Cerberus error dictionary."""
error_messages
=
[]
for
field
,
msgs
in
errors
.
items
():
if
isinstance
(
msgs
,
dict
):
for
f
,
m
in
msgs
.
items
():
error_messages
.
append
(
dict
(
field
=
f
,
message
=
m
,
code
=
error_codes
[
'validation_error'
],
))
else
:
error_messages
.
append
(
dict
(
field
=
field
,
message
=
msgs
,
code
=
error_codes
[
'validation_error'
],
))
return
error_messages
# =========
# Mix-ins
# =========
deposition_decorators
=
[
require_api_auth
(),
error_handler
,
api_request_globals
,
]
class
InputProcessorMixin
(
object
):
"""Mix-in class for validating and processing deposition input data."""
input_schema
=
draft_data_extended_schema
def
validate_input
(
self
,
deposition
,
draft_id
=
None
):
"""Validate input data for creating and update a deposition."""
v
=
APIValidator
()
draft_id
=
draft_id
or
deposition
.
get_default_draft_id
()
metadata_schema
=
deposition
.
type
.
api_metadata_schema
(
draft_id
)
if
metadata_schema
:
schema
=
self
.
input_schema
.
copy
()
schema
[
'metadata'
]
=
metadata_schema
else
:
schema
=
self
.
input_schema
# Either conform to dictionary schema or dictionary is empty
if
not
v
.
validate
(
request
.
json
,
schema
)
and
\
request
.
json
:
abort
(
400
,
message
=
"Bad request"
,
status
=
400
,
errors
=
filter_validation_errors
(
v
.
errors
),
)
def
process_input
(
self
,
deposition
,
draft_id
=
None
):
"""Process input data."""
# If data provided, process it
if
request
.
json
:
if
draft_id
is
None
:
# Defaults to `_default' draft id unless specified
draft
=
deposition
.
get_or_create_draft
(
request
.
json
.
get
(
'draft_id'
,
deposition
.
get_default_draft_id
()
)
)
else
:
draft
=
deposition
.
get_draft
(
draft_id
)
# Process data
dummy_form
,
validated
,
result
=
draft
.
process
(
request
.
json
.
get
(
'metadata'
,
{}),
complete_form
=
True
)
# Validation failed to abort
if
not
validated
:
abort
(
400
,
message
=
"Bad request"
,
status
=
400
,
errors
=
filter_draft_errors
(
result
),
)
if
validated
and
request
.
json
.
get
(
'completed'
,
False
):
draft
.
complete
()
# =========
# Resources
# =========
class
DepositionListResource
(
Resource
,
InputProcessorMixin
):
"""Collection of depositions."""
method_decorators
=
deposition_decorators
def
get
(
self
):
"""List depositions.
:param type: Upload type identifier (optional)
"""
args
=
list_parser
.
parse_args
()
result
=
Deposition
.
get_depositions
(
user
=
current_user
,
type
=
args
[
'type'
]
or
None
)
return
map
(
lambda
o
:
o
.
marshal
(),
result
)
@require_header
(
'Content-Type'
,
'application/json'
)
@require_oauth_scopes
(
'deposit:write'
)
def
post
(
self
):
"""Create a new deposition."""
# Create deposition (uses default deposition type unless type is given)
d
=
Deposition
.
create
(
current_user
,
request
.
json
.
get
(
'type'
,
None
))
# Validate input data according to schema
self
.
validate_input
(
d
)
# Process input data
self
.
process_input
(
d
)
# Save if all went fine
d
.
save
()
return
d
.
marshal
(),
201
def
put
(
self
):
abort
(
405
)
def
delete
(
self
):
abort
(
405
)
def
head
(
self
):
abort
(
405
)
def
options
(
self
):
abort
(
405
)
def
patch
(
self
):
abort
(
405
)
class
DepositionResource
(
Resource
,
InputProcessorMixin
):
"""Deposition item."""
method_decorators
=
deposition_decorators
def
get
(
self
,
resource_id
):
"""Get a deposition."""
return
Deposition
.
get
(
resource_id
,
user
=
current_user
)
.
marshal
()
def
post
(
self
,
resource_id
):
abort
(
405
)
@require_header
(
'Content-Type'
,
'application/json'
)
@require_oauth_scopes
(
'deposit:write'
)
def
put
(
self
,
resource_id
):
"""Update a deposition."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
self
.
validate_input
(
d
)
self
.
process_input
(
d
)
d
.
save
()
return
d
.
marshal
()
@require_oauth_scopes
(
'deposit:write'
)
def
delete
(
self
,
resource_id
):
"""Delete existing deposition."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
d
.
delete
()
return
""
,
204
def
head
(
self
,
resource_id
):
abort
(
405
)
def
options
(
self
,
resource_id
):
abort
(
405
)
def
patch
(
self
,
resource_id
):
abort
(
405
)
class
DepositionDraftListResource
(
Resource
):
"""Deposition draft collection."""
method_decorators
=
deposition_decorators
def
get
(
self
,
resource_id
):
"""List all drafts."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
return
map
(
lambda
x
:
d
.
type
.
marshal_draft
(
x
),
d
.
drafts_list
)
def
post
(
self
,
resource_id
):
abort
(
405
)
def
put
(
self
,
resource_id
):
abort
(
405
)
def
delete
(
self
,
resource_id
):
abort
(
405
)
def
head
(
self
,
resource_id
):
abort
(
405
)
def
options
(
self
,
resource_id
):
abort
(
405
)
def
patch
(
self
,
resource_id
):
abort
(
405
)
class
DepositionDraftResource
(
Resource
,
InputProcessorMixin
):
"""Deposition draft item."""
method_decorators
=
deposition_decorators
input_schema
=
draft_data_schema
def
get
(
self
,
oauth
,
resource_id
,
draft_id
):
"""Get a deposition draft."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
return
d
.
type
.
marshal_draft
(
d
.
get_draft
(
draft_id
))
def
post
(
self
,
resource_id
,
draft_id
):
abort
(
405
)
@require_header
(
'Content-Type'
,
'application/json'
)
@require_oauth_scopes
(
'deposit:write'
)
def
put
(
self
,
resource_id
,
draft_id
):
"""Update a deposition draft."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
self
.
validate_input
(
d
,
draft_id
)
self
.
process_input
(
d
,
draft_id
)
d
.
save
()
def
delete
(
self
,
resource_id
,
draft_id
):
abort
(
405
)
def
head
(
self
,
resource_id
,
draft_id
):
abort
(
405
)
def
options
(
self
,
resource_id
,
draft_id
):
abort
(
405
)
def
patch
(
self
,
resource_id
,
draft_id
):
abort
(
405
)
class
DepositionActionResource
(
Resource
):
"""Representation of deposition action.
Primarily used to execute the underlyinh workflow.
"""
method_decorators
=
deposition_decorators
def
get
(
self
,
resource_id
,
action_id
):
abort
(
405
)
@require_oauth_scopes
(
'deposit:actions'
)
def
post
(
self
,
resource_id
,
action_id
):
"""Run an action."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
return
d
.
type
.
api_action
(
d
,
action_id
)
def
put
(
self
,
resource_id
,
action_id
):
abort
(
405
)
def
delete
(
self
,
resource_id
,
action_id
):
abort
(
405
)
def
head
(
self
,
resource_id
,
action_id
):
abort
(
405
)
def
options
(
self
,
resource_id
,
action_id
):
abort
(
405
)
def
patch
(
self
,
resource_id
,
action_id
):
abort
(
405
)
class
DepositionFileListResource
(
Resource
):
"""Represents a collection of deposition files."""
method_decorators
=
deposition_decorators
def
get
(
self
,
resource_id
):
"""Get deposition list of files."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
return
map
(
lambda
f
:
d
.
type
.
marshal_file
(
f
),
d
.
files
)
@require_header
(
'Content-Type'
,
'multipart/form-data'
)
@require_oauth_scopes
(
'deposit:write'
)
def
post
(
self
,
resource_id
):
"""Upload a file."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
# Bail-out early if not permitted (add_file will also check, but then
# we already uploaded the file)
if
not
d
.
authorize
(
'add_file'
):
raise
ForbiddenAction
(
'add_file'
,
d
)
uploaded_file
=
request
.
files
[
'file'
]
filename
=
secure_filename
(
request
.
form
.
get
(
'filename'
)
or
uploaded_file
.
filename
)
df
=
DepositionFile
(
backend
=
DepositionStorage
(
d
.
id
))
if
df
.
save
(
uploaded_file
,
filename
=
filename
):
try
:
d
.
add_file
(
df
)
d
.
save
()
except
FilenameAlreadyExists
as
e
:
df
.
delete
()
raise
e
return
d
.
type
.
marshal_file
(
df
),
201
@require_header
(
'Content-Type'
,
'application/json'
)
@require_oauth_scopes
(
'deposit:write'
)
def
put
(
self
,
resource_id
):
"""Sort files in collection."""
if
not
isinstance
(
request
.
json
,
list
):
abort
(
400
,
message
=
"Bad request"
,
status
=
400
,
errors
=
[
dict
(
message
=
"Expected a list"
,
code
=
error_codes
[
"validation_error"
],
)],
)
v
=
APIValidator
()
for
file_item
in
request
.
json
:
if
not
v
.
validate
(
file_item
,
file_schema_list
):
abort
(
400
,
message
=
"Bad request"
,
status
=
400
,
errors
=
map
(
lambda
x
:
dict
(
message
=
x
,
code
=
error_codes
[
"validation_error"
]
),
v
.
errors
),
)
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
for
file_item
in
request
.
json
:
if
not
d
.
get_file
(
file_item
[
'id'
]):
raise
FileDoesNotExists
(
file_item
[
'id'
])
# Sort files raise ForbiddenAction if not authorized
d
.
sort_files
(
map
(
lambda
x
:
x
[
'id'
],
request
.
json
))
d
.
save
()
return
map
(
lambda
f
:
d
.
type
.
marshal_file
(
f
),
d
.
files
)
def
delete
(
self
,
resource_id
):
abort
(
405
)
def
head
(
self
,
resource_id
):
abort
(
405
)
def
options
(
self
,
resource_id
):
abort
(
405
)
def
patch
(
self
,
resource_id
):
abort
(
405
)
class
DepositionFileResource
(
Resource
):
"""Represent a deposition file."""
method_decorators
=
deposition_decorators
def
get
(
self
,
resource_id
,
file_id
):
"""Get a deposition file."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
df
=
d
.
get_file
(
file_id
)
if
df
is
None
:
abort
(
404
,
message
=
"File does not exist"
,
status
=
404
)
return
d
.
type
.
marshal_file
(
df
)
@require_oauth_scopes
(
'deposit:write'
)
def
delete
(
self
,
resource_id
,
file_id
):
"""Delete existing deposition file."""
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
# Sort files raise ForbiddenAction if not authorized
df
=
d
.
remove_file
(
file_id
)
if
df
is
None
:
abort
(
404
,
message
=
"File does not exist"
,
status
=
404
)
df
.
delete
()
d
.
save
()
return
""
,
204
def
post
(
self
,
resource_id
,
file_id
):
abort
(
405
)
@require_header
(
'Content-Type'
,
'application/json'
)
@require_oauth_scopes
(
'deposit:write'
)
def
put
(
self
,
resource_id
,
file_id
):
"""Update a deposition file - i.e. rename it."""
v
=
APIValidator
()
if
not
v
.
validate
(
request
.
json
,
file_schema
):
abort
(
400
,
message
=
"Bad request"
,
status
=
400
,
errors
=
map
(
lambda
x
:
dict
(
message
=
x
,
code
=
error_codes
[
"validation_error"
]
),
v
.
errors
),
)
d
=
Deposition
.
get
(
resource_id
,
user
=
current_user
)
df
=
d
.
get_file
(
file_id
)
if
not
d
.
type
.
authorize_file
(
d
,
df
,
'update_metadata'
):
raise
ForbiddenAction
(
'update_metadata'
,
df
)
new_name
=
secure_filename
(
request
.
json
[
'filename'
])
if
new_name
!=
request
.
json
[
'filename'
]:
abort
(
400
,
message
=
"Bad request"
,
status
=
400
,
errors
=
[
dict
(
message
=
"Not a valid filename"
,
code
=
error_codes
[
"validation_error"
]
)],
)
df
.
name
=
new_name
d
.
save
()
return
d
.
type
.
marshal_file
(
df
)
def
head
(
self
,
resource_id
,
file_id
):
abort
(
405
)
def
options
(
self
,
resource_id
,
file_id
):
abort
(
405
)
def
patch
(
self
,
resource_id
,
file_id
):
abort
(
405
)
#
# Register API resources
#
def
setup_app
(
app
,
api
):
api
.
add_resource
(
DepositionListResource
,
'/api/deposit/depositions/'
,
)
api
.
add_resource
(
DepositionResource
,
'/api/deposit/depositions/<string:resource_id>'
,
)
api
.
add_resource
(
DepositionFileListResource
,
'/api/deposit/depositions/<string:resource_id>/files/'
,
)
api
.
add_resource
(
DepositionDraftListResource
,
'/api/deposit/depositions/<string:resource_id>/metadata/'
,
)
api
.
add_resource
(
DepositionDraftResource
,
'/api/deposit/depositions/<string:resource_id>/metadata/'
'<string:draft_id>'
,
)
api
.
add_resource
(
DepositionActionResource
,
'/api/deposit/depositions/<string:resource_id>/actions/'
'<string:action_id>'
,
)
api
.
add_resource
(
DepositionFileResource
,
'/api/deposit/depositions/<string:resource_id>/files/<string:file_id>'
,
)
# Register scopes
with
app
.
app_context
():
from
invenio.modules.oauth2server.models
import
Scope
from
invenio.modules.oauth2server.registry
import
scopes
scopes
.
register
(
Scope
(
'deposit:write'
,
group
=
'Deposit'
,
help_text
=
'Allow upload (but not publishing).'
,
))
scopes
.
register
(
Scope
(
'deposit:actions'
,
group
=
'Deposit'
,
help_text
=
'Allow publishing of uploads.'
,
))
Event Timeline
Log In to Comment