-
-
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.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!!!
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.