Upload document via REST API

I’ve been reading the docs, comments here, and GitLab on how to upload a document.

Here is what I figured out so far:

curl --location 'http://127.0.0.1:8080/api/v4/documents/upload/' \
--header 'Authorization: Token 5f10c9406d99209a0065a5da4862b86dced18053' \
--form 'document_type_id="4"' \
--form 'file=@"/C:/Users/chris/OneDrive/Documents/Screenshot_1.png"'

I will expand on this once I fully figure out what I am doing.

Here is a NodeRed flow that:

  • Uploads document,
  • Adds document to correct cabinet,
  • Adds metadata value.

Required NodeRed palettes:

  • node-red-dashboard
  • node-red-contrib-ui-upload

Import flow:

[{"id":"f6f2187d.f17ca8","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"549c7642da358bdd","type":"http request","z":"f6f2187d.f17ca8","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":750,"y":200,"wires":[["12e98b9c214451b9"]]},{"id":"12e98b9c214451b9","type":"function","z":"f6f2187d.f17ca8","name":"store token","func":"if(msg.statusCode === 200) {\n    node.log('obtain auth token success');\n    flow.set('mayan.auth.token', msg.payload.token);\n    flow.set('mayan.auth.valid', true);\n} else {\n    node.warn('obtain auth token failed!');\n    flow.set('mayan.auth.token', null);\n    flow.set('mayan.auth.valid', false);\n}\nreturn null;","outputs":0,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":950,"y":200,"wires":[]},{"id":"5eaa7606e06ff73a","type":"inject","z":"f6f2187d.f17ca8","name":"run at startup","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":240,"y":100,"wires":[["df6d1478d9c32f01"]]},{"id":"df6d1478d9c32f01","type":"function","z":"f6f2187d.f17ca8","name":"settings","func":"flow.set('mayan.api.url_base', 'http://mayan-app-1:8000/api/v4');\nflow.set('mayan.credential.username', 'admin');\nflow.set('mayan.credential.password', 'wmHMhj5tEa');\nreturn msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":100,"wires":[["e69bc0c889878581"]]},{"id":"e69bc0c889878581","type":"function","z":"f6f2187d.f17ca8","name":"build request","func":"msg.method = \"POST\";\nmsg.url = `${flow.get('mayan.api.url_base')}/auth/token/obtain/`;\nmsg.headers = {\n    'Content-Type': 'application/json',\n    'Accept': 'application/json'\n};\nmsg.payload = {\n    'username': flow.get('mayan.credential.username'),\n    'password': flow.get('mayan.credential.password')\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":200,"wires":[["549c7642da358bdd"]]},{"id":"4c73c5ff9e205c68","type":"ui_upload","z":"f6f2187d.f17ca8","group":"70994f3107e78413","title":"Upload Invoice Without Purchase Order","accept":"","name":"upload document","order":1,"width":0,"height":5,"chunk":256,"transfer":"binary","x":230,"y":340,"wires":[["26b16b339747d9c1"]]},{"id":"bc9b5396b35ad48c","type":"http request","z":"f6f2187d.f17ca8","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":750,"y":420,"wires":[["ae20ac342ca148c9"]]},{"id":"22a3feb2cf341c76","type":"function","z":"f6f2187d.f17ca8","name":"build request","func":"msg.method = \"POST\";\nmsg.url = `${flow.get('mayan.api.url_base')}/documents/upload/`;\nmsg.headers = {\n    'Content-Type': 'multipart/form-data',\n    'Authorization': `Token ${flow.get('mayan.auth.token')}`\n}\nmsg.payload = {\n  \"document_type_id\": msg.mayan.document_type_id,\n  \"file\": {\n    \"value\": msg.mayan.document__file_buffer,\n    \"options\": {\n      \"filename\": msg.mayan.document__file_name\n    }\n  }\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":420,"wires":[["bc9b5396b35ad48c"]]},{"id":"ae20ac342ca148c9","type":"function","z":"f6f2187d.f17ca8","name":"response","func":"if (msg.statusCode > 299) {\n    node.warn('api call failed!');\n    return null;\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":940,"y":420,"wires":[["651e3acca7c91317"]]},{"id":"043652f6aed56a6a","type":"function","z":"f6f2187d.f17ca8","name":"build request","func":"msg.method = \"POST\";\nmsg.url = `${flow.get('mayan.api.url_base')}/cabinets/${msg.mayan.cabinet_id}/documents/add/`;\nmsg.headers = {\n  'Content-Type': 'application/json',\n  'Accept': 'application/json',\n  'Authorization': `Token ${flow.get('mayan.auth.token')}`\n};\nmsg.payload = {\n  'document': `${msg.mayan.document_id}`\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":500,"wires":[["c9240507c578b4ca"]]},{"id":"651e3acca7c91317","type":"function","z":"f6f2187d.f17ca8","name":"mayan data","func":"msg.mayan = {\n    ... msg.mayan,\n    'cabinet_id': 9,\n    'document_id': msg.payload.id\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":350,"y":500,"wires":[["043652f6aed56a6a"]]},{"id":"c9240507c578b4ca","type":"http request","z":"f6f2187d.f17ca8","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":750,"y":500,"wires":[["a754d9038b0e8dc0"]]},{"id":"08a94d42a1c2eba0","type":"comment","z":"f6f2187d.f17ca8","name":"upload document","info":"","x":220,"y":380,"wires":[]},{"id":"0b93ae2506f22e70","type":"comment","z":"f6f2187d.f17ca8","name":"move to cabinet","info":"","x":220,"y":460,"wires":[]},{"id":"26b16b339747d9c1","type":"function","z":"f6f2187d.f17ca8","name":"mayan data","func":"msg.mayan = {\n    'document_type_id': 4,\n    'document__file_name': msg.file.name,\n    'document__file_buffer': msg.payload\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":350,"y":420,"wires":[["22a3feb2cf341c76"]]},{"id":"269e7e5d177d6a9f","type":"comment","z":"f6f2187d.f17ca8","name":"add metadata","info":"","x":210,"y":540,"wires":[]},{"id":"be978c727309e89d","type":"comment","z":"f6f2187d.f17ca8","name":"obtain auth token","info":"","x":220,"y":160,"wires":[]},{"id":"a754d9038b0e8dc0","type":"function","z":"f6f2187d.f17ca8","name":"response","func":"if (msg.statusCode > 299) {\n    node.warn('api call failed!');\n    return null;\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":940,"y":500,"wires":[["34fb9392b3ad3bb0"]]},{"id":"117e43e8c99afd1e","type":"function","z":"f6f2187d.f17ca8","name":"build request","func":"msg.method = \"POST\";\nmsg.url = `${flow.get('mayan.api.url_base')}/documents/${msg.mayan.document_id}/metadata/`;\nmsg.headers = {\n  'Content-Type': 'application/json',\n  'Accept': 'application/json',\n  'Authorization': `Token ${flow.get('mayan.auth.token')}`\n};\nmsg.payload = {\n  'metadata_type_id': `${msg.mayan.metadata_type_id}`,\n  'value': `${msg.mayan.metadata__value}`\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":580,"wires":[["34182535f35e04d4"]]},{"id":"34fb9392b3ad3bb0","type":"function","z":"f6f2187d.f17ca8","name":"mayan data","func":"msg.mayan = {\n    ... msg.mayan,\n    'metadata_type_id': 6,\n    'metadata__value': \"4000\"\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":350,"y":580,"wires":[["117e43e8c99afd1e"]]},{"id":"34182535f35e04d4","type":"http request","z":"f6f2187d.f17ca8","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":750,"y":580,"wires":[["1586a8f07cda7524"]]},{"id":"1586a8f07cda7524","type":"function","z":"f6f2187d.f17ca8","name":"response","func":"if (msg.statusCode > 299) {\n    node.warn('api call failed!');\n    return null;\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":940,"y":580,"wires":[["3ccda02042ae74fb"]]},{"id":"3ccda02042ae74fb","type":"debug","z":"f6f2187d.f17ca8","name":"completion","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"'complete'","targetType":"jsonata","statusVal":"","statusType":"auto","x":350,"y":660,"wires":[]},{"id":"dea3a619a0e3ce8c","type":"comment","z":"f6f2187d.f17ca8","name":"http://nodered:1880/ui","info":"","x":240,"y":300,"wires":[]},{"id":"70994f3107e78413","type":"ui_group","name":"Upload File","tab":"6ab69d53c11131b3","order":1,"disp":false,"width":"12","collapse":false,"className":""},{"id":"6ab69d53c11131b3","type":"ui_tab","name":"Document Upload","icon":"dashboard","disabled":false,"hidden":false}]
2 Likes

Thanks for sharing this process. I don’t use nodered, so I can’t download your flow to review.

Can you tell me when you are adding the metadata values, do you need to pull the metadata fields first, or are you hardcoding them into your flow?

Also are you using HTTP PATCH when you do?

Thanks

I am hardcoding the Metadata Type and its value for now; using POST.

image

1 Like

That is very helpful. I’m working doing something very similar with n8n. So this is great timing for me!

Thank you

2 Likes

Interesting, first time I am learning about n8n.

I’ve been using it for a couple of years, it’s been very dependable.

Can you post the full URL with headers, parameters, and body?

When I look at the Mayan API docs, I can’t seem to get it to match what I see here.

Thanks

POST /api/v4/documents/upload/ HTTP/1.1
Host: 127.0.0.1:8080
Authorization: Token 5f10c9406d99209a0065a5da4862b86dced18053
Content-Length: 321
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="document_type_id"

4
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="/C:/Users/chris/OneDrive/Documents/Screenshot_1.png"
Content-Type: image/png

(data)
------WebKitFormBoundary7MA4YWxkTrZu0gW--

Thank you. If I could ask, could you also show me the url for when you added the metadata?

Thanks

URL: POST http://mayan-app-1:8000/api/v4/documents/72/metadata/

Headers: { "Content-Type":"application/json", "Accept":"application/json", "Authorization":"Token 5f10c9406d99209a0065a5da4862b86dced18053" }

Payload: {"metadata_type_id":"6","value":"4000"}

Thank you. That’s what I thought it should be. I’ve been having trouble making mine work. I must have a typo or something.

@dbayer Do you also get an error saying document_type_id: a valid integer is required?

Are you referring to the file upload or the metadata update?

I believe you need an existing document to upload the file to. That is probably where the error is coming from.

For uploading the file, I used this URL (where 72 is the document ID)

https://test.mywebsite.com/api/v4/documents/72/files/
Body Parameters
Form-Data
file_new: binary file
action_name: append (in this example, I already had one file in the document)

For the Metadata update I used this URL (where 72 is the Document ID)

https://test.mywebsite.com/api/v4/documents/72/metadata/
Body Parameters
metadata_type_id: meta data type id
value: meta data

Ah I tried the somewhat new /documents/upload endpoint. But maybe this one is currently broken or does not work how I thought it would.

I can’t check right now, but I think that also needs an existing document to point to.

Don’t forget you can view and test the REST API in the Tools Menu in Mayan.

Hi

I was able to upload a file to a document testing with this curl command:

curl -X POST -u “admin:password” -F “action_name=replace” -F “file_new=@test.pdf” http://127.0.0.1:9080/api/v4/documents/1/files/;

This is equivalent on how it is documented here in python:
https://docs.mayan-edms.com/apps/rest_api/index.html
I’m using mayan container image with tag : mayanedms/mayanedms:s4.7

Regarding OpenApi Spec:

It seems, that this is not correctly documented in the openapi spec.
There is no mention of the parameter “file_new” in the whole spec.
Also there is no usage of the format “multipart/form-data” in the spec.
Without this, generated openapi clients (like for angular or java), are treating the file parameter as an string instead of correctly a File Class.

Could this maybe be adjusted on your side in the code ?
Hit me up, if you need more informations.

Peeking on other projects that we’ve done, this example would correctly generate a openapi client, which can handle file uploads (just an snippet from one of our openapi specs):

      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - pdfFile
              properties:
                pdfFile:
                  type: string
                  format: binary

additionaly
i just found out, that the Django REST Documentation correctly lists the parameter “file_new” when looking it up like this:
http://localhost:9080/api/v4/documents/1/files/?format=api
and pressing the OPTIONS button in the upper right corner

Scrolling down, there is then the parameter listed:

            "file_new": {
                "type": "file upload",
                "required": true,
                "read_only": false,
                "label": "File new",
                "help_text": "Binary content for the new file."
            },

So maybe just a problem with generating the swagger-ui/openapi spec ?
Maybe we can somehow extract the OPTIONS informations from Django to a correct openapi spec file.

ok further digging into the problem:
it seems that the used library for generating the openapi spec (“drf-yasg”) doesnt support fully generating correctly oas 3.0 spec.
https://drf-yasg.readthedocs.io/en/stable/readme.html#openapi-3-0-note

They suggest to use the library drf-spectacular.

Maybe this would do the trick.

1 Like