-
-
Save CodeShakingSheep/e6efe69f2f7082ceb590e8ce68fa2bfc to your computer and use it in GitHub Desktop.
| # 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') |
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
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
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/stacksnot really sure what is going on there.
Hm, not sure either. You could try to omit
/index.phpfrom 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!!!
Hm, not sure either. You could try to omit
/index.phpfrom 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.