[Feature] Add a fileUpload endpoint to the REST API #641

Closed
opened 2026-02-04 20:39:50 +03:00 by OVERLORD · 6 comments
Owner

Originally created by @hwelch-fle on GitHub (Jan 28, 2025).

As it stands, one of the only missing pieces of the REST API is an endpoint to uploading files and getting a valid Attachment response back. If we had this, we could easily script full imports of other systems into Planka. Currently we can only exfil, but being able to migrate would be huge.

Reference to Plankapy discussion that started this: https://github.com/plankanban/planka/discussions/1000#discussioncomment-11984028_

Originally created by @hwelch-fle on GitHub (Jan 28, 2025). As it stands, one of the only missing pieces of the REST API is an endpoint to uploading files and getting a valid `Attachment` response back. If we had this, we could easily script full imports of other systems into Planka. Currently we can only exfil, but being able to migrate would be huge. Reference to Plankapy discussion that started this: https://github.com/plankanban/planka/discussions/1000#discussioncomment-11984028_
Author
Owner

@meltyshev commented on GitHub (Jan 29, 2025):

Hi! You can upload attachment files using POST /api/cards/:cardId/attachments.

The parameters are:

cardId: {
  type: 'string',
  regex: /^[0-9]+$/,
  required: true,
},
requestId: {
  type: 'string',
  isNotEmptyString: true,
},

// The file itself should be in the "file" parameter

If the upload is successful, the result will be:

{
  item: {
    [attachment data]
  }
}
@meltyshev commented on GitHub (Jan 29, 2025): Hi! You can upload attachment files using `POST /api/cards/:cardId/attachments`. The parameters are: ``` cardId: { type: 'string', regex: /^[0-9]+$/, required: true, }, requestId: { type: 'string', isNotEmptyString: true, }, // The file itself should be in the "file" parameter ``` If the upload is successful, the result will be: ``` { item: { [attachment data] } } ```
Author
Owner

@hwelch-fle commented on GitHub (Jan 29, 2025):

Turns out when you're using urllib only, doing multipart/form-data requests is a pain haha. Managed to get it working with this:

def _post_file(self, file_path: Path, file_name: str) -> bytes:
    """Multipart formatting is hard"""
    # Set headers for file upload
    headers = self.headers.copy() # Make a copy of the headers
    
    headers['Connection'] = 'keep-alive'
    headers['Accept'] = '*/*'
    headers['Accept-Encoding'] = 'gzip, deflate, br'
    
    # Get file data and MIME type
    # Default to binary if MIME type is not found
    mime_type = guess_type(file_name)[0] or 'application/octet-stream' 
    
    # Generate boundary for multipart form data
    boundary_uuid = uuid4().hex
    boundary = f'--{boundary_uuid}'
    
    # Add multipart form data headers with boundary
    headers['Content-Type'] = f'multipart/form-data; boundary={boundary}'
    
    # Get payload parts
    payload_disposition = f'Content-Disposition: form-data; name="file"; filename="{file_name}"'.encode('utf-8')
    payload_content_type = f"Content-Type: {mime_type}\r\n\r\n".encode('utf-8')
    file_data = file_path.read_bytes()
    
    # Construct payload
    payload = BytesIO()
    payload.write(f'--{boundary}'.encode('utf-8'))     # Boundary Start
    payload.write(b'\r\n')                                         # New line
    payload.write(payload_disposition)                    # Content-Disposition
    payload.write(b'\r\n')                                         # New line
    payload.write(payload_content_type)                 # Content-Type
    payload.write(file_data)                                      # File data
    payload.write(b'\r\n')                                         # New line
    payload.write(f'--{boundary}--'.encode('utf-8'))  # EOF
    payload.write(b'\r\n')                                         # New line
    payload = payload.getvalue()                            # Get payload as bytes
    
    # Add content length to headers
    headers['Content-Length'] = len(payload)
    
    return self._open(Request(
        self.endpoint, 
        headers=headers, 
        method='POST', 
        data=payload
    ))

So plankapy now supports posting attachments and backgrounds!

@hwelch-fle commented on GitHub (Jan 29, 2025): Turns out when you're using `urllib` only, doing multipart/form-data requests is a pain haha. Managed to get it working with this: ```python def _post_file(self, file_path: Path, file_name: str) -> bytes: """Multipart formatting is hard""" # Set headers for file upload headers = self.headers.copy() # Make a copy of the headers headers['Connection'] = 'keep-alive' headers['Accept'] = '*/*' headers['Accept-Encoding'] = 'gzip, deflate, br' # Get file data and MIME type # Default to binary if MIME type is not found mime_type = guess_type(file_name)[0] or 'application/octet-stream' # Generate boundary for multipart form data boundary_uuid = uuid4().hex boundary = f'--{boundary_uuid}' # Add multipart form data headers with boundary headers['Content-Type'] = f'multipart/form-data; boundary={boundary}' # Get payload parts payload_disposition = f'Content-Disposition: form-data; name="file"; filename="{file_name}"'.encode('utf-8') payload_content_type = f"Content-Type: {mime_type}\r\n\r\n".encode('utf-8') file_data = file_path.read_bytes() # Construct payload payload = BytesIO() payload.write(f'--{boundary}'.encode('utf-8')) # Boundary Start payload.write(b'\r\n') # New line payload.write(payload_disposition) # Content-Disposition payload.write(b'\r\n') # New line payload.write(payload_content_type) # Content-Type payload.write(file_data) # File data payload.write(b'\r\n') # New line payload.write(f'--{boundary}--'.encode('utf-8')) # EOF payload.write(b'\r\n') # New line payload = payload.getvalue() # Get payload as bytes # Add content length to headers headers['Content-Length'] = len(payload) return self._open(Request( self.endpoint, headers=headers, method='POST', data=payload )) ``` So `plankapy` now supports posting attachments and backgrounds!
Author
Owner

@meltyshev commented on GitHub (Jan 29, 2025):

Wow, that was fast!

@meltyshev commented on GitHub (Jan 29, 2025): Wow, that was fast!
Author
Owner

@hwelch-fle commented on GitHub (Jan 29, 2025):

Wow, that was fast!

I'm still learning urllib, definitely used some resources and tried to copy the structure of the request I was seeing in the browser when I did it normally. It was upset when I fed it a byte array, but using a BytesIO object and writing one line at a time made it work

@hwelch-fle commented on GitHub (Jan 29, 2025): > Wow, that was fast! I'm still learning `urllib`, definitely used some resources and tried to copy the structure of the request I was seeing in the browser when I did it normally. It was upset when I fed it a byte array, but using a `BytesIO` object and writing one line at a time made it work
Author
Owner

@hwelch-fle commented on GitHub (Jan 29, 2025):

I think that might have been the last back-end piece for Plankapy, Now I just need to work on implementing some interface stuff and maybe a CLI. Have you had a chance to test out the latest version yet?

@hwelch-fle commented on GitHub (Jan 29, 2025): I think that might have been the last back-end piece for Plankapy, Now I just need to work on implementing some interface stuff and maybe a CLI. Have you had a chance to test out the latest version yet?
Author
Owner

@meltyshev commented on GitHub (Feb 2, 2025):

Have you had a chance to test out the latest version yet?

I'm very busy migrating changes from v1 to v2, but I'll definitely test it afterward!

@meltyshev commented on GitHub (Feb 2, 2025): > Have you had a chance to test out the latest version yet? I'm very busy migrating changes from v1 to v2, but I'll definitely test it afterward!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/planka#641