By Steffen Prohaska
The package nog-error
helps handling errors.
Meteor provides a mechanism to prevent server-side errors from being sent to the
client: It sends Meteor.Error
as is. Errors of a different type, however, are
only reported to the server log. Either a generic error 500 is sent to the
client instead or the content of the field sanitizedError
if it contains
a Meteor.Error
.
nog-error
provides functions to maintain four information streams: {full, sanitized} x {user, developer}
. reason
contains information that can be
displayed to users as is. details
contains information for developers. The
error type NogError.Error
is used on the server. The sanitizedError
is
reported to the client.
Errors are stored upon creation to the Mongo collection nogerror.errorLog
with
a TTL index (see http://docs.mongodb.org/manual/core/index-ttl/) that removes
error documents after some time (default: 5 days). Only server-side errors are
currently stored. Errors have a short token
, which can be useful to retrieve
them from the log:
db.nogerror.errorLog.findOne({token: 'ma4ddq'})
Errors are created from a spec. It is an open question whether we should use
many detailed specs or use fewer general purpose specs and report details in the
reason. See nog-error-specs.coffee
for a list of error specs.
nog-error
also provides a default error handler and a template for displaying
client-side errors.
createError(spec, context)
creates an error object. It uses the general
structure from spec
and the situation-specific information from context
. If
context
has a field cause
, it will be used to derive an error history, which
simply is an array of the previous causes. The error object is of type
NogError.Error
with a sanitizedError
of type Meteor.Error
that would be
reported to the client.
spec
is usually one of the specs that are defined in nog-error-specs.coffee
.
The full developer documentation contains a list in a separate section below.
The format of spec
, which is needed to define a new error, is described below.
context
is an object that may be used to provide information about the context
in which the error occurred. The context information will be used when creating
the error object and also be stored in the error log:
reason
(String, optional): A message for the user that overrides the default message from the specs.details
(String, optional): A detailed message to developers that overrides the default message from the specs.cause
(Error, optional): Will be used to derive an error history.- Some specs require further fields that are used to construct the messages.
Usage examples:
{
ERR_BLOB_UPLOAD
ERR_LOGIC
ERR_UNKNOWN_USERID
createError
nogthrow
} = NogError
fn = ->
...
if not found
nogthrow ERR_UNKNOWN_USERID, {uid}
...
nogthrow ERR_LOGIC, {reason: 'Unknown entry type.'}
someAction (err, res) ->
if err
@onerror createError ERR_BLOB_UPLOAD,
cause: err
reason: "
Failed to get upload URL for part #{part.partNumber}; aborting
after #{@nTries} tries.
"
New error codes are defined (by convention in nog-error-specs.coffee
) with
a spec
object with the following fields:
errorCode
(String
, by convention all uppercaseERR_<topic>_<details>
) identifies the type of error.statusCode
(Number
) is a HTTP status code that is used if the error is reported via HTTP (from the REST API).reason
(String | (context) -> String
): Text that explains the reasons (for a user); either static or generated by a function.details
(String | (context) -> String
): Text that explains the details (for a developer); either static or generated by a function.contextPattern
(Match Pattern
): A Meteor match pattern that is used to validate thecontext
object. A warning is reported to the console, if the pattern does not match.sanitized
(null | 'full' | Object
) controls how the sanitized error for the client is constructed. Fornull
, an unspecified error with codeNOGERR
will be used. Forfull
, the complete information that is available on the server will be used.sanitized
can also be an Object with the following optional fields{errorCode: String, reason: String | (context) -> String, details: String | (context) -> String}
) to control the details.
Example specs:
{
errorCode: 'ERR_APIKEY_DELETE'
statusCode: 403
sanitized: 'full'
reason: 'Failed to create API key.'
}
{
errorCode: 'ERR_S3_CREATE_MULTIPART'
statusCode: 502
sanitized: null
reason: 'Failed to create S3 multipart upload.'
details: (ctx) -> "
Failed to call createMultipartUpload with S3 bucket `#{ctx.s3Bucket}`,
and object key `#{ctx.s3ObjectKey}`.
"
contextPattern:
s3Bucket: String
s3ObjectKey: String
}
nogthrow()
calls createError()
and throws the error.
Usage example:
{
nogthrow
ERR_BLOB_ABORT_PENDING
} = NogError
someFunction = (opts) ->
try
doSomething()
catch cause
nogthrow ERR_BLOB_ABORT_PENDING, {sha1: opts.sha1, cause}
An error handler that stores the error for {{> errorDisplay}}
.
Example usage with method call on client:
aFunction = ->
...
Meteor.call 'something', (err, res) ->
if err?
return NogError.defaultErrorHandler(err)
...
Packages typically call the handler indirectly via a configurable hook:
NogBlob =
onerror: NogError.defaultErrorHandler
...
aFunction = ->
...
Meteor.call 'something', (err, res) ->
if err?
NogBlob.onerror(err)
A Meteor template that displays the errors that have been reported to the error handler.
See nog-error-specs.coffee
for a list of error specs.
nog-setting
helps managing Meteor settings.
defSettings()
helps managing Meteor settings. key
is a dot path in
Meteor.settings
. val
is the default value that will be used if none is
provided by the environment. help
is a short help text, starting with the
last part of <key>
by convention. match
is a Meteor check match pattern to
validate the settings value.
The recommendation is to use defSettings()
in a separate file
nog-<package>-settings.js
to define all settings early during package
initialization.
Example nog-<package>-settings.js
:
import { defSettings } from 'meteor/nog-settings';
defSetting({
key: 'public.upload.concurrentUploads',
val: 3,
help: `
\`concurrentUploads\` limits the number of concurrent file uploads from
a browser.
`,
match: matchPositiveNumber,
});
nog-access
provides access control that is inspired by AWS:
- http://docs.aws.amazon.com/IAM/latest/UserGuide/PoliciesOverview.html,
- http://docs.aws.amazon.com/IAM/latest/UserGuide/AccessPolicyLanguage_EvaluationLogic.html.
Access is determined from a list of statements such as:
{
principal: 'role:users'
action: 'nog-blob/upload'
effect: 'allow'
}
{
principal: /// ^ username : [^:]+ $ ///
action: 'nog-content/create-repo'
effect: (opts) ->
userName = opts.principal.split(':')[1]
if userName is opts.ownerName
'allow'
else
'ignore'
}
{
principal: 'role:users'
action: 'nog-blob/upload'
effect: (opts) ->
if not config.uploadSizeLimit
'ignore'
else if config.uploadSizeLimit is 0
'ignore'
else if not opts?.size?
'ignore'
else if opts.size <= config.uploadSizeLimit
'allow'
else
{
effect: 'deny'
reason: "
Upload is larger than the size limit of #{config.uploadSizeLimit}
Bytes.
"
}
}
Access control uses the roles package alanning:roles
. The current user is
expanded to an array of principals ['username:<username>', 'userid:<userid>', 'role:<role.0>', 'ldapgroup:<group.0>', '...]
, and each expanded principal is
tested against the access statements. Access is granted if any of the
statements has the effect: 'allow'
and no statement has the effect: 'deny'
.
Logged-in users that have no role assigned are tested as principals
['username:<username>', 'userid:<userid>', 'guests']
.
Logged-out connections are tested as principal ['anonymous']
.
principal
can be a string (exact match) or a regular expression.
effect
can be a function (opts) -> {effect: 'access' | 'deny' | 'ignore', reason: String}
that is evaluated on the opts
that are passed to the access
check functions. The Meteor user object is available as opts.user
if a user
is known. The original opts
passed to checkAccess()
cannot contain a field
user
.
The list of access control statements can be manipulated with
removeStatements()
and addStatement()
(see below). This should only be
done during startup. It is not yet clear whether we will maintain most
statements centrally in the default list or use addStatement()
to add them
if needed.
If the user doc contains user.scopes
, a special pre-check will be performed
whether the action is permitted. opts.scopes
is an array of objects
{action: String, opts: Object}
. The access check action
and opts
are
compared against each scope. An equality match for one scope is required for
the access check to proceed to the statement processing phase. Access is
denied otherwise.
Usage example:
NogBlob =
checkAccess: ->
## Use nog-access if available (weak dependency).
if Meteor.isServer
if (p = Package['nog-access'])?
console.log '[nog-blob] using nog-access default policy.'
NogBlob.checkAccess = p.NogAccess.checkAccess
else
console.log '
[nog-blob] default access control disabled, since nog-access is not
available.
'
Meteor.methods
'startMultipartUpload': (opts) ->
check opts, {name: String, size: Number, sha1: isSha1}
if not Meteor.isServer then return
NogBlob.checkAccess Meteor.user(), 'nog-blob/upload',
_.pick(opts, 'size')
...
checkAccess(user, action, opts)
works similar to Meteor.check()
. user
may
be an object or an user id or null
. checkAccess()
throws if access is
denied or simply returns if access is granted. checkAccess()
can be used for
access control in Meteor methods, publish functions and REST request handlers.
Example use in method:
Meteor.methods
'startMultipartUpload': (opts) ->
...
NogBlob.checkAccess Meteor.user(), 'nog-blob/upload', opts
...
Example use in publish function:
Meteor.publish 'nog-blob/blobs', (sha1s) ->
NogAccess.checkAccess @userId, 'nog-blob/upload', {sha1s}
...
Since an exception is thrown if access is denied, the subscription is terminated
in this case. Access deny errors will be reported to the client's onStop()
subscribe callback. Example:
Meteor.subscribe 'nog-blob/blobs', [],
onStop: (err) ->
console.error err
The publish function will not be re-run by the server if the logged-in user
changes. The client code needs to explicitly call Meteor.subscribe()
again.
If this is a problem, testAccess()
, which returns false
if access is denied,
may be the better alternative. Example:
Meteor.publish 'nog-blob/blobs', (sha1s) ->
if not NogAccess.testAccess @userId, 'nog-blob/upload', {sha1s}
return null
...
Example use in REST API handler:
NogAccess.checkAccess req.auth?.user, action, opts
req.auth.user
is added by nog-auth
during signature verification.
testAccess(user, action, opts)
works similar to checkAccess()
but returns
true
(access) or false
(deny) instead of throwing and exception.
testAccess(action, opts)
on the client works similar to testAccess()
on the
server but returns null
if the result is not yet available, because the
response from the server is pending. The result is reactively updated when the
server call completes. If callback
is provided, it is called with the result
when it becomes available.
testAccess_ready(action, opts)
can be used to test whether the result is
available.
Template helper example:
Template.repoView.helpers
mayModify: ->
...
NogAccess.testAccess 'nog-content/modify', {ownerName, repoName}
Flow router middleware example:
requireUserOrGuest = (path, next) ->
NogAccess.testAccess 'isUser', (err, isUser) ->
NogAccess.testAccess 'isGuest', (err, isGuest) ->
if isUser or isGuest
next()
else
next('/sign-in')
{{testAccess action}}
can be used in a template to test access. It calls
NogAccess.testAccess(action)
.
{{testAccess_ready action}}
can be used to test whether the test results is
available.
Example:
if testAccess_ready 'nog-blob/upload'
if testAccess 'nog-blob/upload'
+uploadView
else
| You cannot upload files.
else
| loading...
Keyword arguments are passed as an opts
object to NogAccess.testAccess()
.
Example:
if testAccess 'nog-content/modify' ownerName='foo' repoName='bar'
+repoView
else
| You cannot modify the repo.
It may be clearer to write a helper function that performs the toggle check if
an opts
object is needed.
{{testAccess_ready action [kwopts]}}
tests whether the test result is
available.
configure()
updates NogAccess.config
with the provided opts
:
uploadSizeLimit
(Number >= 0
, default:Meteor.settings.public.upload.uploadSizeLimit
or0
) limits the allowed blob upload size. Use0
to disable the limit.
The active configuration.
addStatement(statement)
adds a statement to the access control list. See the
introduction above for the format of statement
.
removeStatetments(selector)
removes the statements that match selector
from
the access control list. selector
can contain only a single field action
.
Statements whose action matches (exact string comparison) are removed.
nog-rest
implements server-side routing for a REST API. It uses
signature-based authentication if the package nog-auth
is available.
Routes are registered with NogBlob.actions(prefix, actions)
(see details
below).
Access check of actions should be implemented with NogAccess.checkAccess()
from package nog-access
.
Links to resources should be returned as JSON objects with an href
member and
other alternative identifiers, such as an id
or a sha1
. Each resource
should contain a self reference in member _id
.
Usage example:
if Meteor.isServer
actions = NogBlob.api.blobs.actions()
NogRest.actions '/api/blobs', actions
With NogBlob.api.blobs.actions()
, for example, implemented as follows:
class BlobsApi
constructor: (opts) ->
@blobs = opts.blobs
actions: () ->
[
{ method: 'GET', path: '/:blob', action: @get_blob }
{ method: 'GET', path: '/:blob/content', action: @get_blob_content }
]
# Use `=>` to bind the actions to access this instance's state.
get_blob: (req) =>
{params, baseUrl} = req
params = _.pick params, 'blob'
check params, { blob: isSha1 }
action = 'nog-blob/GET-blob'
NogAccess.checkAccess req.auth?.user, action, req.params
blob = @blobs.findOne params.blob
if not blob?
nogthrow ERR_BLOB_NOT_FOUND, {blob: params.blob}
res = _.pick blob, 'size', 'status', 'confirmations', 'sha1'
res._id =
id: blob._id,
href: Meteor.absoluteUrl(baseUrl[1..] + '/' + blob._id)
res.content =
href: share.getSignedDownloadUrl
sha1: params.blob
filename: params.blob + '.dat'
res
...
Example response JSON:
{
"data": {
"_id": {
"href": "http://localhost:3000/api/blobs/31968d2e8b58e29e63851cb4b340216026f11f69",
"id": "31968d2e8b58e29e63851cb4b340216026f11f69"
},
"confirmations": [
{
"date": "2015-04-27T10:02:31.313Z",
"message": "..."
}
],
"content": {
"href": "https://..."
},
"sha1": "31968d2e8b58e29e63851cb4b340216026f11f69",
"size": 11,
"status": "available"
},
"statusCode": 200
}
NogBlob.actions(prefix, actions)
adds routes that start with prefix
.
actions
is an array of {method: String, path: String, action: callback}
.
prefix
and path
use Express-style syntax as describe at
https://github.com/component/path-to-regexp.
The action callback(req)
receives an HTTP request object, parsed as usual:
URL query in req.params
, parsed JSON body in req.body
. In addition to the
usual fields, req.auth.user
contains a Meteor user if the request signature
has been verified by nog-auth
. req.baseUrl
contains the part of the URL
that was matched by prefix
. It can be used in an action callback to create
URLs that use the same prefix:
href = Meteor.absoluteUrl(baseUrl[1..] + '/' + blob._id)
The action callback either returns a result
object or throws an error.
A result
will be send via HTTP with status code 200 and a JSON body:
{
"statusCode": 200
"data": result
}
If result
contains a field statusCode
, its value will be used instead for
the HTTP code and in the JSON body:
{
"statusCode": result.statusCode
"data": _.omit(result, 'statusCode')
}
As a special case, the callback can return a redirect result
:
{
statusCode: 307
location: "https://..."
}
It will be translated to the expected HTTP redirect.
An error will be translated to an HTTP error status code and a JSON body such as:
{
"errorCode": "ERR_MATCH",
"message": "Match error: not a sha1 in field blob",
"statusCode": 422
}
configure()
updates the active configuration with the provided opts
:
checkRequestAuth
(Function
, default:NogAuth.checkRequestAuth
): The authentication hook (see below).
checkRequestAuth(req)
is expected to add req.auth.user
with the
authenticated user or to throw if the authentication fails. req
is a HTTP
request object.
nog-auth
provides signature-based authentication of HTTP requests.
See the apidoc for a details description of the signature process.
The Meteor template {{> nogApiKeys}}
provides a UI to manage keys. Secret
keys are encrypted before they are stored in MongoDB. The master keys must be
provided in Meteor.settings.NogAuthMasterKeys
as an array of key objects
[{keyid: String, secretkey: String}]
. The first key is the primary key. Old
keys can be provided to support key rotation. nog-auth
will re-encrypt all
keys with the primary key when on startup. The following commands may be
useful to generate random ids and secrets:
head -c 100 /dev/random | openssl dgst -sha256 | head -c 20 # id
head -c 100 /dev/random | openssl dgst -sha256 | head -c 40 # secret
checkRequestAuth(req)
authenticates an HTTP request. It throws if the
authentication fails. It adds the Meteor user that owns the key as
req.auth.user
if the request was successfully authenticated. If the signing
key has scopes
(see createKey()
), they will be added as
req.auth.user.scopes
.
checkRequestAuth()
is used as the authentication hook in nog-rest
.
signRequest(key, req)
signs an HTTP request object with the key
, which is an
object {keyid: String, secretkey: String}
.
createKey()
creates a new API key for the user id opts.keyOwnerId
after
an access check that user
has permission to create a key.
createKeySudo(opts)
creates a new API key for the user id opts.keyOwnerId
without access check. It returns the key object {keyid, secretkey}
. The
secret key is encrypted with the primary master key before it is stored in
MongoDB.
opts
can contain a comment and scopes. opts.comment
will be displayed in
{{> nogApiKeys}}
. opts.scopes
(an array of {action: String, opts: Object}
) will be returned by checkRequestAuth()
as req.auth.user.scopes
.
The scopes can be used by NogAccess
to restrict the actions that the key can
be used for.
deleteKey()
delete the access key with id opts.keyid
after an access check
that user
has permission to delete the key.
If opts.keyOnwerId
(a user id) is present, it will be used as an additional
selector when finding the key. Since keyid
is assumed to be unique,
keyOnwerId
is only a additional safety measure.
Same as deleteKey()
but without access check.
A UI widget to create and delete API keys for the user in the data context.
Example:
with currentUser
+nogApiKeys
configure()
updates the active configuration with the provided opts
:
onerror
(Function
, default:NogError.defaultErrorHandler
) is used to report errors.checkAccess
(Function
, defaultNogAccess.checkAccess
if available) is used for access control.
The hook onerror(err)
is called with errors on the client.
The hook NogAuth.checkAccess(user, action, opts)
is called to check whether
a user has permissions to manage API keys. See package nog-access
,
specifically NogAccess.checkAccess()
.
nog-content
implements a git-like, content-addressable data model.
The design overview from March 2015 in 2015_fuimages_meteor-spikes:design-overview_2015-03.md may contain further ideas that are not yet implemented.
The REST API is described in detail in the separate doc: apidoc.
The entry point is a repo. It contains mutable state, primarily refs
. refs
are like git refs. Initially, branches/*
is used for branches (instead of
heads/
in git). Other prefixes may be useful later (like tags).
staging
will be used for preparing a commit before actually committing it.
staging
is not yet implemented in nog-content; it has been used in spikes and
is described in the design overview document. staging
works like a local
temporary branch with a commit that is repeatedly amended until it is ready to
be committed. It seems reasonable to use a temporary commit. It can be used to
store the state of the edit form, such as the draft of the commit message. An
alternative would be to use a different data structure for preparing commits
that would be more similar to git's index. This could be useful if the editing
operation needs more state, such as multiple versions of a file for conflict
resolution. For now, a temporary commit seems fine.
The next level is a commit. Like a git commit, it contains information like authors, dates, a message, and so on. A commit points to parent commits and to a tree.
A tree contains a dictionary of metadata and a list of entries of format
{type: object|tree, sha1: <id>}
. It is a recursive data structure. The leaf
nodes are objects. Objects also contain metadata, and can point to a blob.
A blob represents a binary object that is stored in object storage (like S3).
All immutable objects have ids that are computed as sha1s over a canonical EJSON
representation. The documents stored in MongoDB may contain additional
non-essential fields that are not part of the canonical representation. The
most obvious example is _id
, which is the computed sha1. Another candidate is
touchTime
to store when the document was used, which might be relevant when
implementing a time-based garbage collection scheme.
The Mongo collection repos
contains the repositories. Repositories contain
mutable state. A repo has a random _id
and a unique full repo name, which is
composed from the repo owner name and the repo name: <ownerName>/<repoName>
.
The Mongo collection commits
contains the immutable commit entries.
The Mongo collection trees
contains the immutable tree entries.
The Mongo collection objects
contains the immutable object entries.
The Mongo collection blobs
is a reference to NogBlob.blobs
if the packages
nog-blob
is available (weak dependency) and null
otherwise.
contentId(content)
computes the id for content
, which must contain only
valid fields. The calling code, for example, must remove fields that start
with underscore. One way is to use NogContent.stripped()
as in
contentId(stripped(content))
.
strip(content)
removes special internal fields like _id
and _idversion
from content
. content
is modified in place.
stripped(content)
returns a copy of content
without internal fields.
configure()
updates the active configuration with the provided opts
:
-
checkAccess
(Function
, defaultNogAccess.checkAccess
if available) is used for access control. -
testAccess
(Function
, defaultNogAccess.testAccess
if available) is used for access control.
The hook NogContent.checkAccess(user, action, opts)
is called to check whether
a user has the necessary upload and download permissions. See package
nog-access
.
The feature toggle optStrictRepoMembership
(default: true
) controls whether
strict repo membership checks are enabled. If active, entries can only be
accessed via a repo when they are reachable via a ref or when they have been
recently added to the repo. This check should be activated if some kind of
strict content sharing permissions are used, such as sharing only with selected
user circles. If active, nog-content
will configure nog-blob
to check
whether blobs are reachable from a repo.
NogContent.api.repos.actions()
returns an action array that can be plucked
into nog-rest
to provide a REST API.
The REST API is described in detail in the separate doc: apidoc.
The Python example content-testapp/public/tools/bin/test-create-content-py
demonstrates the REST API.
If nog-blob
is used, NogBlob.api.blobs.actions()
and
NogBlob.api.upload.actions()
must be mounted at corresponding paths.
Usage example:
if Meteor.isServer
NogRest.actions '/api/repos', NogContent.api.repos.actions()
NogRest.actions '/api/repos/:ownerName/:repoName/db/blobs',
NogBlob.api.blobs.actions()
NogRest.actions '/api/repos/:ownerName/:repoName/db/blobs',
NogBlob.api.upload.actions()
The object NogContent.call
provides Meteor methods that are used internally,
such as NogContent.createRepo()
.
Due to a bug in the client-side SHA1 computation in browsers, correct blob data was stored under an incorrect blob id in a few cases during early development. The blobs and objects became part of the commit history. We wanted to keep the history but somehow mark the incorrect objects.
Since entries are immutable, the inconsistent ids cannot be modified but must
remain part of the immutable history. To handle such situations, content
entries can have an optional field errata
. Example:
{
"errata": [{ "code": "ERA201609a" }]
}
The meaning of the errata code is deployment-specific. The recommended format
is ERA<year-month-char>
, for example `ERA201609a'. Admins can use this key
to document deployment-specific information about the issue.
This package provides utilities to display errata in the UI, based on a description in the public settings. Example:
{
"public": {
"errata": [
{
"code": "ERA201609a",
"description": "An incorrect data checksum has been stored for this file during upload. You can download a copy of the correct file from repo '<a href=\"/nog/era201609a/files\">nog/era201609a</a>'. Then upload the correct file here and remove this file in order to permanently fix the issue and get rid of this message."
}
]
}
}
nog-blob
implements content-addressable file storage on AWS S3. The S3 object
key is the sha1 of the file content. The sha1 is computed in the browser before
uploading the content. Uploaded objects are stored in the MongoDB collection
blobs
, which is available as NogBlob.blobs
.
The REST API is described in apidoc-blobs and apidoc-upload.
A Mongo.Collection
with information about the uploaded blobs. _id
is the
content sha1.
Clients are automatically subscribed to a subset that is relevant for the current uploads from the client.
uploadFile(file, callbacks)
starts an upload of a web File
object. It
returns an id for the client-only collection NogBlob.files
immediately and
calls done(err, res)
upon completion. res
is an object {_id: String, filename: String, size: Number, sha1: String}
.
The second argument can be an object opts
with functions done(err, res)
,
and onwarning(err)
. It so, onwarning()
is called for reporting
intermediate warnings instead of the default NogBlob.onerror()
. Eventually,
done()
is called as described in the previous paragraph.
Usage example:
template(name='upload')
form
.form-group
input#files(type='file' name='files[]' multiple)
Template.upload.events
'change #files': (e) ->
e.preventDefault()
for f in e.target.files
id = NogBlob.uploadFile f, (err, res) ->
if err
return console.log 'failed to upload file', err
# Do something with res; for example call server to add it.
Meteor.call 'addObject',
name: res.filename
blob: res.sha1
Session.set 'currentUpload', id
A client-only collection that provides a reactive data source to track upload progress.
fileHelpers
is an object with template helper functions that can be used to
implement a UI to display file upload progress. The helper functions expect
a document from NogBlob.files
in the data context.
Usage example:
template(name='currentUpload')
.row
if haveUpload
with file
.col-md-3
span #{name}
.col-md-9
.progress
div(
class="progress-bar {{uploadCompleteClass}}",
role="progressbar",
style="width: {{progressWidth}}%"
)
currentUpload = -> Session.get('currentUpload')
Template.currentUpload.helpers
haveUpload: -> currentUpload()?
file: -> NogBlob.files.findOne currentUpload()
Template.currentUpload.helpers _.pick(
NogBlob.fileHelpers, 'name', 'progressWidth', 'uploadCompleteClass'
)
{{> uploadHeading}}
and {{> uploadItem}}
are templates that illustrated
how to display a list of uploads. We probably will not use them as is in the
production app. Either we improve them or we build a custom UI using the
fileHelpers
described above.
template(name='upload')
+uploadHeading
hr
each uploads
+uploadItem
Template.upload.helpers
uploads: -> NogBlob.files.find()
The template {{> aBlobHref blob=<sha1> name=<filename>}}
inserts an <a>
element that when clicked will download the blob from S3 and save it as the
specified filename.
configure()
updates the active configuration with the provided opts
:
onerror
(Function
, default:NogError.defaultErrorHandler
) is used to report errors.checkAccess
(Function
, defaultNogAccess.checkAccess
if available) is used for access control.repoSets
(instance ofNogContent.RepoSets
orfalse
, default:false
when used without packagenog-content
andtrue
when used withnog-content
) is used internally by the packagenog-content
to inject an implementation that checks whether a blob is reachable from a repo. Checks are currently only implemented in theapi.*.actions
but not for method calls. See source for details.- See source
nog-blob.coffee
for further configuration options.
The currently active configuration.
The hook onerror(err)
is called with errors on the client.
The hook NogAuth.checkAccess(user, action, opts)
is called to check whether
a user has the necessary upload and download permissions. See package
nog-access
.
NogBlob.api.blobs.actions()
returns an action array that can be plucked into
nog-rest
to provide a REST API.
The mount path must contain :ownerName
and :repoName
when used with
repoSets
for repo membership checks, which is the default when package
nog-content
is part of the app.
The REST API is described in apidoc-blobs.
Usage examples:
if Meteor.isServer
NogRest.actions '/api/blobs', NogBlob.api.blobs.actions()
if Meteor.isServer
NogRest.actions '/api/repos/:ownerName/:repoName/db/blobs',
NogBlob.api.blobs.actions()
NogBlob.api.uploads.actions()
returns an action array that can be plucked into
nog-rest
to provide a REST API for uploading blobs. The upload.actions()
must be mounted at the same path as the blobs.actions()
.
The mount path must contain :ownerName
and :repoName
when used with
repoSets
for repo membership checks, which is the default when package
nog-content
is part of the app.
The REST API is described in apidoc-upload.
The Python example blob-testapp/public/tools/bin/test-upload-py
demonstrates
the preferred way of using the REST API for uploading data.
The Bash example blob-testapp/public/tools/bin/test-upload
also demonstrates
how to upload data, but the example is not ideal. It ignores the initial
parts
, and it constructs URLs from identifiers instead of using the provided
href
fields.
Usage examples:
if Meteor.isServer
NogRest.actions '/api/blobs', NogBlob.api.blobs.actions()
NogRest.actions '/api/blobs', NogBlob.api.upload.actions()
if Meteor.isServer
NogRest.actions '/api/repos/:ownerName/:repoName/db/blobs',
NogBlob.api.blobs.actions()
NogRest.actions '/api/repos/:ownerName/:repoName/db/blobs',
NogBlob.api.uploads.actions()
The object NogBlob.call
provides Meteor methods that are used internally, such
as NogBlob.call.startMultipartUpload()
or NogBlob.call.getBlobDownloadURL()
.
nog-files
implements a file browser, which should be as non-technical as
possible. A UI inspired by Dropbox is probably suitable.
Only a subset of functions is documented here. See the source for further details.
spec
must provide selector functions and may provide access control
functions. The selectors return either the name of a Meteor template, which
will be used for rendering, or null
to indicate that it should be ignored.
The access controls either return a control object or null
to indicate that
it should be ignored.
The following selectors must be present:
-
view(treeContext)
: The view template. You can returnnogFilesBundleView
to use a generic bundle view for a tree that should not be modified. -
icon(entryContext)
: The icon template for a list view. You can returnnogFilesBundleIcon
to use a generic bundle icon for a tree that should not be modified.
The following access controls may be present:
treePermissions(treeContext)
: Can be used to restrict the actions that may be performed in a file listing. Return{write: false}
to disable all operations that would modify the tree.
The most relevant fields in treeContext
are (see the code in nog-files-ui.*
for details; the context is the same as for nog-tree reprs):
commitId
(sha1) andcommit
(full object).namePath
(Array of Strings): Names of entries along the resolved path from the tree root.numericPath
(Array of Numbers): Indices of entries along the resolved path from the tree root.contentPath
(Array of Objects): Full information about entries along the resolved path from the tree root. Each object contains the index in the parent tree entries inidx
; the name of the entry inname
; the type of the entry intype
; the entry content incontent
.last
: An alias to the last entry incontentPath
.tree
: The root tree in the same format as thecontentPath
elements. The root tree itself is not part of thecontentPath
.repo
: The repo object.ref
,refTreePath
,refType
, andtreePath
: Information about the path (see code for details).
The relevant fields in entryContext
are:
context.parent
: ThetreeContext
(as described above) for the parent tree.context.child.content
: The content of the entry for which the icon shall be returned.
Individual packages can directly register representations, which requires
a package dependency on nog-files
. An alternative approach is to configure
the repr spec in the main app. We do not yet know, which architecture works
better in practice.
entryView()
is used internally to select the template for displaying an
entry.
entryIcon()
is used internally to select the template used for the icon in
a file list view.
nog-flow
implements a workspace view for workspace repositories that provides
a workflow order to collect data and programs, run programs, and inspect the
results.
NogFlow.call
provides methods to modify the content of repositories, for
instance NogFlow.call.addPrograms(opts)
or NogFlow.call.renameChildren (opts)
. See the source for more information.
nog-tree
implements a tree browsing user interface to nog repositories for
users that want to access the technical details. Some elements are inspired by
GitHub's web UI to browse git repositories.
Only a subset of functions is documented here. See the source for more information.
registerEntryRepr()
registers a new representation for entries to be used
for tree browsing. spec
must have a function spec.selector(context)
, which
is passed the tree data context. The selector must return either the name of
the Meteor template to use for rendering, or it returns null
to indicate that
the representation should be ignored.
The most relevant fields in the tree data context are (see the code in
nog-tree-ui.*
for details):
commitId
(sha1) andcommit
(full object).namePath
(Array of Strings): Names of entries along the resolved path from the tree root.numericPath
(Array of Numbers): Indices of entries along the resolved path from the tree root.contentPath
(Array of Objects): Full information about entries along the resolved path from the tree root. Each object contains the index in the parent tree entries inidx
; the name of the entry inname
; the type of the entry intype
; the entry content incontent
.last
: An alias to the last entry incontentPath
.tree
: The root tree in the same format as thecontentPath
elements. The root tree itself is not part of thecontentPath
.repo
: The repo object.ref
,refTreePath
,refType
, andtreePath
: Information about the path (see code for details).
Individual packages can directly register representations, which requires a package dependency on nog-tree. An alternative approach is to configure the repr spec in the main app. We do not yet know, which architecture works better in practice.
selectEntryRepr()
is internally used to select, based on the tree data
context, a template for rendering an entry . It returns null
if no entry
repr selector matches, which indicates that the default view should be used.
nog-repr-example
is an example package to illustrate how to add entry
representations for tree and file browsing.
nog-repr-image
implements plugins to handle images in nog-tree
and
nog-files
.
nog-repr-markdown
implements plugins to handle markdown in nog-tree
and
nog-files
.
nog-repr-flow
handles workflow-related entries in nog-tree
and nog-files
.
It currently only configures nog-files
to display certain tree kinds as
bundles. More code will probably be moved from nog-tree
later.
The package nog-multi-bucket
provides mechanisms to manage blobs in multiple
object buckets.
createBucketRouterFromSettings()
creates a multi-bucket router with methods
getDownloadUrl({ blob, filename })
and getImgSrc({ blob, filename })
that
return blob download URLs considering a configured bucket preference order and
bucket health. The mechanism is used by package nog-blob
.
Multi-bucket upload routing is supported through createMultipartUpload({ key })
, which uses the writePrefs
settings (see below) to determine the upload
bucket. getSignedUploadPartUrl()
returns upload URLs for the individual
parts. The upload is completed with completeMultipartUpload()
or canceled
with abortMultipartUpload()
.
The multi-bucket router is configured from package nog-blob
via
Meteor.settings.multiBucket
.
nog-multi-bucket
exports a Meteor check match pattern
matchMultiBucketSettings
, which can be used to validate settings as follows:
import { matchMultiBucketSettings } from 'meteor/nog-multi-bucket';
check(Meteor.settings.multiBucket, matchMultiBucketSettings);
The following are example settings for one AWS S3 bucket, which is assumed to be always healthy, and one Ceph S3 bucket with a health check that reads an object and verifies its content. The Ceph S3 bucket is preferred for download and upload:
{
"multiBucket": {
"readPrefs": ["nog-zib-2", "nog"],
"writePrefs": ["nog-zib-2", "nog"],
"fallback": "nog",
"buckets": [
{
"name": "nog",
"region": "eu-central-1",
"accessKeyId": "AKxxxxxxxxxxxxxxxxxx",
"secretAccessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"check": "healthy"
},
{
"name": "nog-zib-2",
"accessKeyId": "Cxxxxxxxxxxxxxxxxxxx",
"secretAccessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"endpoint": "https://objs2.zib.nogproject.io",
"signatureVersion": "v4",
"check": "getObject",
"checkKey": "_whoami",
"checkContent": "bucket:nog-zib-2",
"checkInterval": "15s"
}
]
},
}
The health check expects an object whose content equals checkContent
. Such
an object can, for example, be created with a properly configured S3cmd as
follows:
echo 'bucket:nog-zib' >'_whoami'
s3cmd put '_whoami' 's3://nog-zib/_whoami'
The AWS key needs s3:PutObject
and s3:GetObject
rights on the S3 bucket
that is used by nog-blob
. The recommended approach to AWS permission
management is to use one AWS IAM user for the application and grant rights via
groups with inline policies (use the custom policy editor). For example:
User nog-app
.
Group nog-s3-get
with policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1420905603000",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::nog/*"
]
}
]
}
Group nog-s3-put
with policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1420905603000",
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::nog/*"
]
}
]
}
The S3 CORS configuration must allow any origin and expose the ETag header (see http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/browser-configuring.html#Cross-Origin_Resource_Sharing__CORS_):
Use XML to configure CORS via the AWS Admin UI:
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
<ExposeHeader>ETag</ExposeHeader>
</CORSRule>
</CORSConfiguration>
A local Docker container with Ceph can be used as an alternative to AWS S3 during development. The manual steps are described below. The repo root contains a Docker Compose file that automates the steps for a basic setup.
Run the Docker image ceph/demo
as follows:
docker run -it --rm \
--name ceph \
-p 10080:80 \
-e NETWORK_AUTO_DETECT=4 \
-e CEPH_DEMO_UID=nog -e CEPH_DEMO_ACCESS_KEY=Cdemo -e CEPH_DEMO_SECRET_KEY=Cdemosecret -e CEPH_DEMO_BUCKET=noglocal \
ceph/demo
Use the following entry in Meteor.settings.multiBucket.buckets
:
{
"name": "noglocal",
"endpoint": "http://localhost:10080",
"accessKeyId": "Cdemo",
"secretAccessKey": "Cdemosecret"
}
Configure an AWS credentials profile in ~/.aws/credentials
:
[cephdemo]
aws_access_key_id=Cdemo
aws_secret_access_key=Cdemosecret
Configure CORS settings. With the following cors.json
:
{
"CORSRules": [
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["PUT", "POST", "GET", "HEAD"],
"MaxAgeSeconds": 3000,
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"]
}
]
}
Run:
aws --profile cephdemo --endpoint-url http://localhost:10080 s3api put-bucket-cors --bucket noglocal --cors-configuration file://cors.json
You can create additional buckets as follows:
aws --profile cephdemo --endpoint-url http://localhost:10080 --region localhost s3 mb s3://noglocal2
DEPRECATED: nog-s3
should not be used anymore. Use nog-multi-bucket
instead.
nog-s3
wraps just enough of the AWS SDK to implement nog-blob
. S3
exposes
only a few sync functions. Errors are translated to NogError.Error
. The
opts
are identical to params of the official AWS SDK:
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html.
configure()
updates the active configuration with the provided opts
:
accessKeyId
(String
, defaultMeteor.settings.AWSAccessKeyId
).secretAccessKey
(String
, defaultMeteor.settings.AWSSecretAccessKey
).region
(String
, defaultMeteor.settings.AWSBucketRegion
).signatureVersion
(s3
orv4
, defaultMeteor.settings.AWSSignatureVersion
orv4
):eu-central-1
requiresv4
.s3ForcePathStyle
(Boolean
, defaultMeteor.settings.AWSS3ForcePathStyle
orfalse
): URL format forfalse
is{bucket}.{region}...
; URL format fortrue
is{endpoint}/{bucket}
. The path style may be useful with alternative S3 implementations, like Ceph RadosGW.endpoint
(String
, defaultMeteor.settings.AWSEndpoint
): The endpoint must accept requests from the server and from client browsers.sslEnabled
(Boolean
, defaultMeteor.settings.AWSSslEnabled
ortrue
).ca
(String
, defaultMeteor.settings.AWSCa
): If present, must be an absolute path to a CA certificate bundle .pem file, which will be loaded and used instead of the CAs that are bundled with Node.
The key needs to have s3:PutObject
and s3:GetObject
rights on the S3 bucket
that is used by nog-blob
. The recommended approach to AWS permission
management is to use one AWS IAM user for the application and grant rights via
groups with inline policies (use the custom policy editor). For example:
User nog-app
.
Group nog-s3-get
with policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1420905603000",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::nog/*"
]
}
]
}
Group nog-s3-put
with policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1420905603000",
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::nog/*"
]
}
]
}
The S3 CORS configuration must allow any origin and expose the ETag header (see http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/browser-configuring.html#Cross-Origin_Resource_Sharing__CORS_):
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
<ExposeHeader>ETag</ExposeHeader>
</CORSRule>
</CORSConfiguration>
See http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#createMultipartUpload-property.
See http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#uploadPart-property.
See http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getObject-property.
See http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#completeMultipartUpload-property.
See http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#abortMultipartUpload-property.
The package nog-test
provides testing infrastructure.
During startup, nog-test
configures Mocha by default to exclude all tagged
test. The tag format is _[A-Z]+_
anywhere in a test name.
To include tests, set Meteor.settings.public.tests.mocha
to an object
{"grep": String, "invert": Boolean}
to specify a test filter. Use {"grep": ".*"}
to include all tests.
testingMethods(methods)
calls Meteor.methods(methods)
. It ignores errors,
so that methods can be defined within tests that may be called repeatedly.
Example:
describe 'some test', ->
savedAccess = null
testingMethods
'testing/nog-auth/disableAccessCheck': ->
if Meteor.isServer
savedAccess = NogAuth.access
NogAuth.configure {access: {check: ->}}
'testing/nog-auth/restoreAccessCheck': ->
if Meteor.isServer
NogAuth.configure {access: savedAccess}
before((next) -> Meteor.call 'testing/nog-auth/disableAccessCheck', next)
after((next) -> Meteor.call 'testing/nog-auth/restoreAccessCheck', next)
it.client 'a test that runs without access control', (next) ->
pause()
is setTimeout
with a human-friendly argument order. Example:
describe 'some context', ->
it 'a test', ->
someWork()
pause 1000, ->
expect(...)
specs = [
{
errorCode: 'ERR_MIGRATION'
statusCode: 500
sanitized: null
reason: 'A database migration failed.'
}
{
errorCode: 'ERR_UNIMPLEMENTED'
statusCode: 500
sanitized: null
reason: 'The operation is not implemented.'
}
# Malformed as in XML structure validation: the format of a value is invalid.
{
errorCode: 'ERR_PARAM_MALFORMED'
statusCode: 422
sanitized: 'full'
reason: 'A parameter was malformed.'
}
# Invalid comes after malformed: the basic structure is ok, but the value is
# semantically invalid, such as out-of-range.
{
errorCode: 'ERR_PARAM_INVALID'
statusCode: 422
sanitized: 'full'
reason: 'A parameter was semantically invalid.'
}
{
errorCode: 'ERR_S3_CREATE_MULTIPART'
statusCode: 502
sanitized: null
reason: 'Failed to create S3 multipart upload.'
details: (ctx) -> "
Failed to call createMultipartUpload with S3 bucket `#{ctx.s3Bucket}`,
and object key `#{ctx.s3ObjectKey}`.
"
contextPattern:
s3Bucket: String
s3ObjectKey: String
}
{
errorCode: 'ERR_S3_COMPLETE_MULTIPART'
statusCode: 502
sanitized: null
reason: 'Failed to complete S3 multipart upload.'
details: (ctx) -> "
Failed to call completeMultipartUpload with S3 bucket `#{ctx.s3Bucket}`,
and object key `#{ctx.s3ObjectKey}`.
"
contextPattern:
s3Bucket: String
s3ObjectKey: String
s3UploadId: String
}
{
errorCode: 'ERR_S3_ABORT_MULTIPART'
statusCode: 502
sanitized: null
reason: 'Failed to abort S3 multipart upload.'
details: (ctx) -> "
Failed to call abortMultipartUpload with S3 bucket `#{ctx.s3Bucket}`,
object key '#{ctx.s3ObjectKey}', and upload id `#{ctx.s3UploadId}`.
"
contextPattern:
s3Bucket: String
s3ObjectKey: String
s3UploadId: String
}
{
errorCode: 'ERR_BLOB_NOT_FOUND'
statusCode: 404
sanitized: 'full'
reason: (ctx) -> "The requested blob '#{ctx.blob}' could not be found."
details: null
contextPattern:
blob: String
}
{
errorCode: 'ERR_BLOB_UPLOAD_START'
statusCode: 502
sanitized: null
reason: 'Failed to start upload.'
details: (ctx) -> "
The server reported an error when calling 'startMultipartUpload' for
file `#{ctx.fileName}`, size #{ctx.fileSize}, sha1 `#{ctx.sha1}`.
"
contextPattern:
fileName: String
fileSize: Number
sha1: String
}
{
errorCode: 'ERR_BLOB_UPLOAD'
statusCode: 500
sanitized: 'full'
reason: 'Blob upload failed.'
}
{
errorCode: 'ERR_BLOB_UPLOAD_EXISTS'
statusCode: 409
sanitized: 'full'
reason: (ctx) -> "
The blob '#{ctx.sha1}' already exists and cannot be uploaded again.
You may continue assuming that the blob is available as if the
upload succeeded.
"
contextPattern:
sha1: String
}
{
errorCode: 'ERR_BLOB_CONFLICT'
statusCode: 409
sanitized: 'full'
reason: (ctx) -> "
The blob '#{ctx.sha1}' already exists, but with a different size.
You should probably contact a system administrator.
"
contextPattern:
sha1: String
}
{
errorCode: 'ERR_BLOB_UPLOAD_WARN'
statusCode: 500
sanitized: 'full'
reason: 'Problem with blob upload that may be resolved later.'
}
{
errorCode: 'ERR_BLOB_COMPUTE_SHA1'
statusCode: 500
sanitized: 'full'
reason: 'Problem computing sha1.'
}
{
errorCode: 'ERR_BLOB_COMPUTE_MD5'
statusCode: 500
sanitized: 'full'
reason: 'Problem computing MD5.'
}
{
errorCode: 'ERR_BLOB_ABORT_PENDING'
statusCode: 502
sanitized: null
reason: "Failed to abort pending upload after timeout."
contextPattern:
sha1: String
}
{
errorCode: 'ERR_DB'
statusCode: 500
sanitized: null
reason: "A database operation unexpectedly failed."
}
{
errorCode: 'ERR_LIMIT'
statusCode: 413
sanitized: 'full'
reason: "The request is larger than a limit."
}
{
errorCode: 'ERR_LIMIT_S3_OBJECT_SIZE'
statusCode: 413
sanitized: 'full'
reason: (ctx) -> "
The upload size (#{ctx.size} Bytes) is greater than the maximum
size supported by S3 (#{ctx.maxSize} Bytes).
"
contextPattern:
size: Number
maxSize: Number
}
{
errorCode: 'ERR_UPLOADID_UNKNOWN'
statusCode: 404
sanitized: 'full'
reason: 'The upload id is unknown.'
}
{
errorCode: 'ERR_UPLOAD_COMPLETE'
statusCode: 502
sanitized: 'full'
reason: 'Failed to complete the S3 multipart upload.'
}
{
errorCode: 'ERR_BLOB_DOWNLOAD'
statusCode: 500
sanitized: 'full'
reason: 'Problem with blob download.'
}
{
errorCode: 'ERR_UNKNOWN_MASTER_KEY'
statusCode: 500
reason: (ctx) -> "Unknown master key id '#{ctx.masterkeyid}'."
contextPattern:
masterkeyid: String
}
{
errorCode: 'ERR_UNKNOWN_USERID'
statusCode: 404
reason: (ctx) -> "Could not find user id '#{ctx.uid}'."
contextPattern:
uid: String
}
{
errorCode: 'ERR_UNKNOWN_USERNAME'
statusCode: 404
reason: (ctx) -> "Could not find user '#{ctx.username}'."
contextPattern:
username: String
}
{
errorCode: 'ERR_UNKNOWN_KEYID'
statusCode: 404
reason: (ctx) -> "Could not find key id '#{ctx.keyid}'."
contextPattern:
keyid: String
}
{
errorCode: 'ERR_AUTH_FIELD_MISSING'
statusCode: 401
sanitized: 'full'
reason: (ctx) -> "Invalid signature (missing #{ctx.missing})."
contextPattern:
missing: String
}
{
errorCode: 'ERR_AUTH_DATE_INVALID'
statusCode: 401
sanitized: 'full'
reason: (ctx) -> "Invalid authdate"
}
{
errorCode: 'ERR_AUTH_SIG_EXPIRED'
statusCode: 401
sanitized: 'full'
reason: (ctx) -> 'Expired signature'
}
{
errorCode: 'ERR_AUTH_KEY_UNKNOWN'
statusCode: 401
sanitized: 'full'
reason: (ctx) -> "Unknown key."
}
{
errorCode: 'ERR_AUTH_SIG_INVALID'
statusCode: 401
sanitized: 'full'
reason: (ctx) -> "Invalid signature."
}
{
errorCode: 'ERR_AUTH_EXPIRES_INVALID'
statusCode: 401
sanitized: 'full'
reason: (ctx) -> "Invalid expires."
}
{
errorCode: 'ERR_AUTH_NONCE_INVALID'
statusCode: 401
sanitized: 'full'
reason: (ctx) -> "Invalid nonce."
}
{
errorCode: 'ERR_ACCESS_DENY'
statusCode: 404
sanitized: 'full'
reason: 'Access denied by policy.'
}
{
errorCode: 'ERR_ACCESS_DEFAULT_DENY'
statusCode: 404
sanitized: 'full'
reason: 'Access denied without policy.'
}
{
errorCode: 'ERR_APIKEY_CREATE'
statusCode: 403
sanitized: 'full'
reason: 'Failed to create API key.'
}
{
errorCode: 'ERR_APIKEY_DELETE'
statusCode: 403
sanitized: 'full'
reason: 'Failed to create API key.'
}
{
errorCode: 'ERR_CONTENT_REPO_EXISTS'
statusCode: 409
sanitized: 'full'
reason: (ctx) -> "The repo `#{ctx.repoFullName}` already exists."
contextPattern:
repoFullName: String
}
{
errorCode: 'ERR_CONTENT_MISSING'
statusCode: 404,
sanitized: 'full'
reason: (ctx) ->
if ctx.object?
"The object `#{ctx.object}` is missing."
else if ctx.tree?
"The tree `#{ctx.tree}` is missing."
else if ctx.blob?
"The blob `#{ctx.blob}` is missing."
else if ctx.commit?
"The commit `#{ctx.commit}` is missing."
else
"Some content is missing."
}
{
errorCode: 'ERR_CONTENT_CHECKSUM'
statusCode: 500,
sanitized: 'full'
reason: (ctx) ->
"Content id checksum error for #{ctx.type} #{ctx.sha1}."
contextPattern:
sha1: String
type: String
}
{
errorCode: 'ERR_REPO_MISSING'
statusCode: 404,
sanitized: 'full'
reason: 'The repo does not exist.'
}
{
errorCode: 'ERR_REF_MISMATCH'
statusCode: 409
sanitized: 'full'
reason: 'The old ref does not match.'
}
{
errorCode: 'ERR_REF_NOT_FOUND'
statusCode: 404
sanitized: 'full'
reason: (ctx) -> "The requested ref '#{ctx.refName}' could not be found."
contextPattern:
refName: String
}
{
errorCode: 'ERR_CONFLICT'
statusCode: 409
sanitized: 'full'
reason: 'The request conflicts with a concurrent request.'
}
{
errorCode: 'ERR_LOST_LOCK'
statusCode: 409
sanitized: 'full'
reason: 'The active request lost its lock to a concurrent request.'
}
{
errorCode: 'ERR_LOGIC'
statusCode: 500
sanitized: null
reason: 'There is a problem with the program logic.'
}
{
errorCode: 'ERR_CREATE_ACCOUNT_USERNAME'
statusCode: 401
sanitized: 'full'
reason: 'Cannot create account: no username.'
}
{
errorCode: 'ERR_CREATE_ACCOUNT_USERNAME_TOOSHORT'
statusCode: 401
sanitized: 'full'
reason: 'Username must be at least 3 characters long.'
}
{
errorCode: 'ERR_CREATE_ACCOUNT_USERNAME_INVALID'
statusCode: 401
sanitized: 'full'
reason: 'Username may only contain the following characters: "a-z", "0-9", "_", and "-".'
}
{
errorCode: 'ERR_CREATE_ACCOUNT_USERNAME_BLACKLISTED'
statusCode: 401
sanitized: 'full'
reason: 'Username is not allowed.'
}
{
errorCode: 'ERR_CREATE_ACCOUNT_USERNAME_GITHUB'
statusCode: 401
sanitized: 'full'
reason: 'Username already exists as a non-github account.'
}
{
errorCode: 'ERR_CREATE_ACCOUNT_USERNAME_GITIMP'
statusCode: 401
sanitized: 'full'
reason: 'Username already exists as a non-gitimp account.'
}
{
errorCode: 'ERR_CREATE_ACCOUNT_USERNAME_GITZIB'
statusCode: 401
sanitized: 'full'
reason: 'Username already exists as a non-gitzib account.'
}
{
errorCode: 'ERR_CREATE_ACCOUNT_EMAIL'
statusCode: 401
sanitized: 'full'
reason: 'Cannot create account: no email address.'
}
{
errorCode: 'ERR_ACCOUNT_DELETE'
statusCode: 403
sanitized: 'full'
reason: 'Cannot delete account.'
}
{
errorCode: 'ERR_CREATE'
statusCode: 400
sanitized: 'full'
reason: 'Failed to create a resource.'
}
{
errorCode: 'ERR_UNKNOWN'
statusCode: 404
sanitized: 'full'
reason: 'A resource is unknown.'
}
{
errorCode: 'ERR_UPDATE'
statusCode: 400
sanitized: 'full'
reason: 'An update failed.'
}
# Example: `meta.workspace` should be an object, but it's not.
{
errorCode: 'ERR_NOT_OF_KIND'
statusCode: 400
sanitized: 'full'
reason: 'An entry is not of the expected kind.'
}
{
errorCode: 'ERR_PROC_TIMEOUT'
statusCode: 503
sanitized: 'full'
reason: 'Request processing took too long.'
}
{
errorCode: 'ERR_API_VERSION'
statusCode: 409
sanitized: 'full'
reason: 'The API version is incompatible.'
}
{
errorCode: 'ERR_UPDATE_SYNCHRO'
statusCode: 500
sanitized: null
reason: 'An update on a synchro failed.'
}
{
errorCode: 'ERR_SYNCHRO_MISSING'
statusCode: 404,
sanitized: null
reason: 'The synchro does not exist.'
}
{
errorCode: 'ERR_SYNCHRO_CONTENT_MISSING'
statusCode: 404,
sanitized: null
reason: (ctx) ->
if ctx.object?
"The synchro object `#{ctx.object}` is missing."
else if ctx.tree?
"The synchro tree `#{ctx.tree}` is missing."
else if ctx.commit?
"The synchro commit `#{ctx.commit}` is missing."
else
"Some synchro content is missing."
}
{
errorCode: 'ERR_SYNCHRO_STATE'
statusCode: 400,
sanitized: null
reason: 'The synchro state is invalid.'
}
{
errorCode: 'ERR_SYNCHRO_SNAPSHOT_INVALID'
statusCode: 500,
sanitized: null
reason: 'The synchro snapshot is invalid.'
}
{
errorCode: 'ERR_SYNCHRO_APPLY_FAILED'
statusCode: 500,
sanitized: null
reason: 'Sync apply failed.'
}
]
for s in specs
NogError[s.errorCode] = s
See apidoc.