Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save CodeShakingSheep/e6efe69f2f7082ceb590e8ce68fa2bfc to your computer and use it in GitHub Desktop.
Save CodeShakingSheep/e6efe69f2f7082ceb590e8ce68fa2bfc to your computer and use it in GitHub Desktop.
Nextcloud Deck Export/Import (Note that all comments will be created from the user account specified in the first lines of the script)
# You need to have the 'requests' module installed, see here: https://pypi.org/project/requests/
import requests
# Note regarding 2FA
# You can either disable 'Enforce 2FA' setting and disable '2FA'. Then you can just use your regular user password.
# Or you can just use an app password, e.g. named 'migration' which you can create in 'Personal settings' --> 'Security'. After successful migration you can delete the app password.
urlFrom = 'https://nextcloud.domainfrom.tld'
authFrom = ('username', 'user password or app password')
urlTo = 'https://nextcloud.domainto.tld'
authTo = ('username', 'user password or app password')
# Deck API documentation: https://deck.readthedocs.io/en/latest/API/
# Use API v1.1 with Deck >= 1.3.0
# For Deck >= 1.0.0 and < 1.3.0 change API version in deckApiPath to v1.0 (leave ocsApiPath unchanged)
# Note that exporting / importing attachments only works with API v.1.1
deckApiPath='index.php/apps/deck/api/v1.1'
ocsApiPath='ocs/v2.php/apps/deck/api/v1.0'
headers={'OCS-APIRequest': 'true', 'Content-Type': 'application/json'}
headersOcsJson={'OCS-APIRequest': 'true', 'Accept': 'application/json'}
def getBoards():
response = requests.get(
f'{urlFrom}/{deckApiPath}/boards',
auth=authFrom,
headers=headers)
response.raise_for_status()
return response.json()
def getBoardDetails(boardId):
response = requests.get(
f'{urlFrom}/{deckApiPath}/boards/{boardId}',
auth=authFrom,
headers=headers)
response.raise_for_status()
return response.json()
def getStacks(boardId):
response = requests.get(
f'{urlFrom}/{deckApiPath}/boards/{boardId}/stacks',
auth=authFrom,
headers=headers)
response.raise_for_status()
return response.json()
def getStacksArchived(boardId):
response = requests.get(
f'{urlFrom}/{deckApiPath}/boards/{boardId}/stacks/archived',
auth=authFrom,
headers=headers)
response.raise_for_status()
return response.json()
def getAttachments(boardId, stackId, cardId):
response = requests.get(
f'{urlFrom}/{deckApiPath}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments',
auth=authFrom,
headers=headers)
response.raise_for_status()
return response.json()
def getAttachment(path):
response = requests.get(
f'{urlFrom}/{path}',
auth=authFrom,
headers=headers)
response.raise_for_status()
return response
def getComments(cardId):
response = requests.get(
f'{urlFrom}/{ocsApiPath}/cards/{cardId}/comments',
auth=authFrom,
headers=headersOcsJson)
response.raise_for_status()
return response.json()
def createBoard(title, color):
response = requests.post(
f'{urlTo}/{deckApiPath}/boards',
auth=authTo,
json={
'title': title,
'color': color
},
headers=headers)
response.raise_for_status()
board = response.json()
boardId = board['id']
# remove all default labels
for label in board['labels']:
labelId = label['id']
response = requests.delete(
f'{urlTo}/{deckApiPath}/boards/{boardId}/labels/{labelId}',
auth=authTo,
headers=headers)
response.raise_for_status()
return board
def createLabel(title, color, boardId):
response = requests.post(
f'{urlTo}/{deckApiPath}/boards/{boardId}/labels',
auth=authTo,
json={
'title': title,
'color': color
},
headers=headers)
response.raise_for_status()
return response.json()
def createStack(title, order, boardId):
response = requests.post(
f'{urlTo}/{deckApiPath}/boards/{boardId}/stacks',
auth=authTo,
json={
'title': title,
'order': order
},
headers=headers)
response.raise_for_status()
return response.json()
def createCard(title, ctype, order, description, duedate, boardId, stackId):
response = requests.post(
f'{urlTo}/{deckApiPath}/boards/{boardId}/stacks/{stackId}/cards',
auth=authTo,
json={
'title': title,
'type': ctype,
'order': order,
'description': description,
'duedate': duedate
},
headers=headers)
response.raise_for_status()
return response.json()
def assignLabel(labelId, cardId, boardId, stackId):
response = requests.put(
f'{urlTo}/{deckApiPath}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel',
auth=authTo,
json={
'labelId': labelId
},
headers=headers)
response.raise_for_status()
def createAttachment(boardId, stackId, cardId, fileType, fileContent, mimetype, fileName):
url = f'{urlTo}/{deckApiPath}/boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments'
payload = {'type' : fileType}
files=[
('file',(fileName, fileContent, mimetype))
]
response = requests.post( url, auth=authTo, data=payload, files=files)
response.raise_for_status()
return response.json()
def createComment(cardId, message):
response = requests.post(
f'{urlTo}/{ocsApiPath}/cards/{cardId}/comments',
auth=authTo,
json={
'message': message
},
headers=headersOcsJson)
response.raise_for_status()
return response.json()
def archiveCard(card, boardId, stackId):
cardId = card['id']
card['archived'] = True
response = requests.put(
f'{urlTo}/{deckApiPath}/boards/{boardId}/stacks/{stackId}/cards/{cardId}',
auth=authTo,
json=card,
headers=headers)
response.raise_for_status()
def copyCard(card, boardIdTo, stackIdTo, labelsMap, boardIdFrom):
createdCard = createCard(
card['title'],
card['type'],
card['order'],
card['description'],
card['duedate'],
boardIdTo,
stackIdTo
)
# copy attachments
attachments = getAttachments(boardIdFrom, card['stackId'], card['id'])
for attachment in attachments:
fileName = attachment['data']
owner = attachment['createdBy']
mimetype = attachment['extendedData']['mimetype']
attachmentPath = attachment['extendedData']['path']
path = f'remote.php/dav/files/{owner}{attachmentPath}'
fileContent = getAttachment(path).content
createAttachment(boardIdTo, stackIdTo, createdCard['id'], attachment['type'], fileContent, mimetype, fileName)
# copy card labels
if card['labels']:
for label in card['labels']:
assignLabel(labelsMap[label['id']], createdCard['id'], boardIdTo, stackIdTo)
if card['archived']:
archiveCard(createdCard, boardIdTo, stackIdTo)
# copy card comments
comments = getComments(card['id'])
if(comments['ocs']['data']):
for comment in comments['ocs']['data']:
createComment(createdCard['id'], comment['message'])
def archiveBoard(boardId, title, color):
response = requests.put(
f'{urlTo}/{deckApiPath}/boards/{boardId}',
auth=authTo,
json={
'title': title,
'color': color,
'archived': True
},
headers=headers)
response.raise_for_status()
# get boards list
print('Starting script')
boards = getBoards()
# create boards
for board in boards:
boardIdFrom = board['id']
# create board
createdBoard = createBoard(board['title'], board['color'])
boardIdTo = createdBoard['id']
print('Created board', board['title'])
# create labels
boardDetails = getBoardDetails(board['id'])
labelsMap = {}
for label in boardDetails['labels']:
createdLabel = createLabel(label['title'], label['color'], boardIdTo)
labelsMap[label['id']] = createdLabel['id']
# copy stacks
stacks = getStacks(boardIdFrom)
stacksMap = {}
for stack in stacks:
createdStack = createStack(stack['title'], stack['order'], boardIdTo)
stackIdTo = createdStack['id']
stacksMap[stack['id']] = stackIdTo
print(' Created stack', stack['title'])
# copy cards
if not 'cards' in stack:
continue
for card in stack['cards']:
copyCard(card, boardIdTo, stackIdTo, labelsMap, boardIdFrom)
print(' Created', len(stack['cards']), 'cards')
# copy archived stacks
stacks = getStacksArchived(boardIdFrom)
for stack in stacks:
# copy cards
if not 'cards' in stack:
continue
print(' Stack', stack['title'])
for card in stack['cards']:
copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap, boardIdFrom)
print(' Created', len(stack['cards']), 'archived cards')
# archive board if it was archived
if(board['archived']):
archiveBoard(board['id'], board['title'], board['color'])
print(' Archived board')
@FlederFau
Copy link

Hey,

First of all, I would like to thank you for your work.

I noticed that for accounts where the personal deck is missing the synchronization breaks off with this error message.

Starting script
Created board Personal
Traceback (most recent call last):
  File "/home/John/Downloads/e6efe69f2f7082ceb590e8ce68fa2bfc-9f2d3ed64f151f158fe6be7b899d3fd78b147e24/nextcloud-deck-export-import.py", line 245, in <module>
    boardDetails = getBoardDetails(board['id'])
  File "/home/John/Downloads/e6efe69f2f7082ceb590e8ce68fa2bfc-9f2d3ed64f151f158fe6be7b899d3fd78b147e24/nextcloud-deck-export-import.py", line 37, in getBoardDetails
    response.raise_for_status()
  File "/usr/lib/python3/dist-packages/requests/models.py", line 943, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://my.example.com/index.php/apps/deck/api/v1.1/boards/1

Unfortunately, it didn't stop at this one mistake. The following error message is clearly too complex for me. I therefore wanted to ask if anyone can help me with this?

Stack Erledigt
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/urllib3/connection.py", line 169, in _new_conn
    conn = connection.create_connection(
  File "/usr/lib/python3/dist-packages/urllib3/util/connection.py", line 96, in create_connection
    raise err
  File "/usr/lib/python3/dist-packages/urllib3/util/connection.py", line 86, in create_connection
    sock.connect(sa)
TimeoutError: [Errno 110] Connection timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 700, in urlopen
    httplib_response = self._make_request(
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 383, in _make_request
    self._validate_conn(conn)
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 1017, in _validate_conn
    conn.connect()
  File "/usr/lib/python3/dist-packages/urllib3/connection.py", line 353, in connect
    conn = self._new_conn()
  File "/usr/lib/python3/dist-packages/urllib3/connection.py", line 174, in _new_conn
    raise ConnectTimeoutError(
urllib3.exceptions.ConnectTimeoutError: (<urllib3.connection.HTTPSConnection object at 0x7f192856c970>, 'Connection to server2.com timed out. (connect timeout=None)')

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/requests/adapters.py", line 439, in send
    resp = conn.urlopen(
  File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 756, in urlopen
    retries = retries.increment(
  File "/usr/lib/python3/dist-packages/urllib3/util/retry.py", line 574, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='server2.com', port=443): Max retries exceeded with url: /index.php/apps/deck/api/v1.1/boards/10/stacks/18/cards (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x7f192856c970>, 'Connection to server2.com timed out. (connect timeout=None)'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/John/Downloads/e6efe69f2f7082ceb590e8ce68fa2bfc-9f2d3ed64f151f158fe6be7b899d3fd78b147e24/Johannah.py", line 274, in <module>
    copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap, boardIdFrom)
  File "/home/John/Downloads/e6efe69f2f7082ceb590e8ce68fa2bfc-9f2d3ed64f151f158fe6be7b899d3fd78b147e24/Johannah.py", line 184, in copyCard
    createdCard = createCard(
  File "/home/John/Downloads/e6efe69f2f7082ceb590e8ce68fa2bfc-9f2d3ed64f151f158fe6be7b899d3fd78b147e24/Johannah.py", line 127, in createCard
    response = requests.post(
  File "/usr/lib/python3/dist-packages/requests/api.py", line 119, in post
    return request('post', url, data=data, json=json, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 544, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 657, in send
    r = adapter.send(request, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/adapters.py", line 504, in send
    raise ConnectTimeout(e, request=request)
requests.exceptions.ConnectTimeout: HTTPSConnectionPool(host='server2.com', port=443): Max retries exceeded with url: /index.php/apps/deck/api/v1.1/boards/10/stacks/18/cards (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x7f192856c970>, 'Connection to server2.com timed out. (connect timeout=None)'))

I would be very grateful to the person and they would take the pain out of copying everything by hand

@maptal
Copy link

maptal commented Nov 20, 2024

Getting this one?!

  Created stack Speicher
Traceback (most recent call last):
  File "bin/deck_ex_import.py", line 263, in <module>
    copyCard(card, boardIdTo, stackIdTo, labelsMap, boardIdFrom)
  File "bin/deck_ex_import.py", line 208, in copyCard
    assignLabel(labelsMap[label['id']], createdCard['id'], boardIdTo, stackIdTo)
KeyError: 11

@MegaMaxIT
Copy link

I am also still getting errors when trying to import certain boards. not all, but some:

Created board 02_Hallways
Traceback (most recent call last):
File "/home/user/Downloads/nextcloud-deck-export-import/nextcloud-deck-export-import2.py", line 265, in
createdStack = createStack(stack['title'], stack['order'], boardIdTo)
File "/home/user/Downloads/nextcloud-deck-export-import/nextcloud-deck-export-import2.py", line 127, in createStack
response.raise_for_status()
File "/usr/lib/python3/dist-packages/requests/models.py", line 943, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://nextcloud.domain.tld/index.php/apps/deck/api/v1.1/boards/76/stacks

not really sure what is going on there.

Hm, not sure either. You could try to omit /index.php from the URL. Also, I just updated the script. So, perhaps you can try with the new version, although I don't think the changes will affect your error.

I encountered the same error and for my case the problem was that some source stacks had "order=None", so I changed the code at line 255 as follows:

    for stack in stacks:
        if stack['order'] is None:
            stack_order = 999
        else:
            stack_order = stack['order']
    createdStack = createStack(stack['title'], stack_order, boardIdTo)

I think it could be done better but it worked, I hope this can help someone else

Thanks to the author of the script!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment