-
-
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') |
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!!!
Getting this one?!