Skip to content

Instantly share code, notes, and snippets.

@chwnam
Last active January 15, 2019 14:09
Show Gist options
  • Save chwnam/a97c5b6c70d274a5ecdaf97761aed6b1 to your computer and use it in GitHub Desktop.
Save chwnam/a97c5b6c70d274a5ecdaf97761aed6b1 to your computer and use it in GitHub Desktop.
드롭박스 공개 공유 URL로부터 모든 하위 폴더에 위치한 파일의 조사, aria2c 프로그램으로 다운로드 받을 수 있도록 인자를 만들어 내는 스크립트입니다.
# /usr/bin/env python2
# -*- coding: utf-8 -*-
import argparse
import csv
import json
import os
import sys
import dropbox
def parse_args():
parser = argparse.ArgumentParser(
description='드롭박스 공개 공유 링크 URL 조사 스크립트. ' +
'access token 문자열은 반드시 환경 변수 \'dropbox_access_token\'으로 지정해야 합니다.'
)
parser.add_argument(
'-c', '--command',
choices=['tree', 'merge', 'aria2'],
help='작업을 지시합니다. tree 명령은 공개 드롭박스 URL에서 재귀적으로 순회하면서 파일의 주요 메타데이터를 추출합니다. ' +
'merge 명령은 두 개의 tree 작업 결과 CSV 파일을 하나로 합칠 때 사용합니다.' +
'aria2 명령은 입력 파일로부터 aria2 --input-file 파라미터를 위한 출력을 가공합니다.'
)
parser.add_argument(
'-i', '--input',
type=argparse.FileType('rb'),
default=None,
help='tree 명령 사용시, 이 프로그램이 이전에 출력한 파일을 입력으로 사용할 수 있습니다. ' +
'이 경우 이 파일을 기준으로 하여, 변경된 파일만 출력으로 나옵니다. ' +
'merge 명령 사용시, 기준 CSV 파일을 지정할 때 사용합니다. ' +
'aria2 명령 사용시, 다운로드 받을 파일 목록을 입력합니다.'
)
parser.add_argument(
'-r', '--incremental',
type=argparse.FileType('rb'),
default=None,
help='merge 명령에만 유효합니다. 새롭게 변경된 파일 목록을 지정합니다. ' +
'기준의 파일 내용에 이 내용을 덮어 씌워 새 파일로 만듭니다.'
)
parser.add_argument(
'-o', '--output',
type=argparse.FileType('wb'),
default=sys.stdout,
help='명령의 결과를 출력합니다. tree, merge 명령의 출력은 CSV 포맷입니다. aria2c 명령은 일반 텍스트 출력입니다.'
)
parser.add_argument(
'--file-limit',
type=int,
default=-1,
help='tree 명령시 지정한 개수의 파일만 조사하고 마칩니다. 시험 용도입니다.'
)
parser.add_argument(
'--link',
help='작업할 드롭박스의 공개 URL 주소를 입력합니다. tree 작업시 필수적으로 입력해야 합니다.',
default=''
)
return parser.parse_args()
class DropboxSharedLinkTask(object):
"""
see: http://dropbox-sdk-python.readthedocs.io/en/latest/
"""
def __init__(self, access_token, shared_link_url, limit=-1):
self.access_token = ''
if access_token:
self.access_token = access_token
self.dbx = dropbox.Dropbox(access_token)
if shared_link_url:
self.shared_link = dropbox.files.SharedLink(shared_link_url)
self.tree_entries = []
self.reference = {}
self.file_limit = limit
self.file_count = 0
self.field_names = ['path', 'id', 'name', 'rev', 'size', 'content_hash', 'url']
@staticmethod
def is_file(metadata):
return isinstance(metadata, dropbox.files.FileMetadata)
@staticmethod
def is_directory(metadata):
return isinstance(metadata, dropbox.files.FolderMetadata)
def get_attachment_item_from_file_metadata(self, path, metadata):
return dict(
zip(
self.field_names,
[
(path + '/' + metadata.name),
metadata.id,
metadata.name,
metadata.rev,
metadata.size,
metadata.content_hash,
''
]
)
)
def tree(self, path):
# shared link 에서는 recursive 가 먹히지 않는다!
response = self.dbx.files_list_folder(path, shared_link=self.shared_link)
for entry in response.entries:
if self.file_count == self.file_limit:
return
if self.is_file(entry):
key = path + '/' + entry.name
skip = self.reference and (key in self.reference) and (entry.rev == self.reference[key]['rev'])
if not skip:
print('retrieving FILE %s' % entry.name)
self.tree_entries.append(self.get_attachment_item_from_file_metadata(path, entry))
self.file_count += 1
elif self.is_directory(entry):
print('retrieving DIR %s' % entry.name)
self.tree(path + '/' + entry.name)
def retrieve_tree(self, reference):
self.tree_entries = []
self.reference = {}
self.file_count = 0
if reference:
reader = csv.DictReader(reference, fieldnames=self.field_names)
next(reader)
for row in reader:
self.reference[row['path']] = row
self.tree('')
def export_entries(self, fp, entries=None):
writer = csv.DictWriter(fp, fieldnames=self.field_names)
writer.writeheader()
if not entries:
entries = self.tree_entries
writer.writerows(entries)
def merge(self, reference, incremental, output):
reader = csv.DictReader(reference, fieldnames=self.field_names)
next(reader)
ref = [row for row in reader]
reader = csv.DictReader(incremental, fieldnames=self.field_names)
next(reader)
inc = {row['path']: row for row in reader}
merged = []
for row in ref:
path = row['path']
if path in inc and row['rev'] != inc[path]['rev']:
merged.append(inc[path])
else:
merged.append(row)
writer = csv.DictWriter(output, fieldnames=self.field_names)
writer.writeheader()
writer.writerows(merged)
def aria2_input_file(self, input_file, output_file):
reader = csv.DictReader(input_file, fieldnames=self.field_names)
next(reader)
items = [row for row in reader]
for item in items:
output_file.write('https://content.dropboxapi.com/2/sharing/get_shared_link_file' + '\n')
output_file.write('\theader=Authorization: Bearer %s\n' % self.access_token)
output_file.write('\theader=Dropbox-API-Arg: %s\n' % (
json.dumps({
'url': self.shared_link.url,
'path': item['path']
})
))
output_file.write('\tout=%s\n' % item['path'].strip('/'))
print('aria2c 명령어는 https://aria2.github.io/manual/en/html/aria2c.html 페이지를 참고하세요.')
print('다음 옵션은 꼭 고려하여 사용하세요.')
print('-d, --dir=<DIR> 다운로드 파일을 받을 디렉토리.')
print('-i, --input-file=<FILE> 이 명령의 결과물을 입력하기 위해.')
print('-j, --max-concurrent-downloads=<N> 동시 다운로드 수 조정.')
print('--dry-run [true|false] true면 다운로드 시도만 해 봅니다.')
print('--allow-overwrite [true|false] 덮어쓰기를 할지 말지 결정합니다.')
print('--auto-file-renaming [true|false] 자동으로 이름 고치기를 허용할지 결정합니다.')
def interruptable_task(try_callback, finally_callback):
message = ''
try:
try_callback()
except KeyboardInterrupt:
message = '사용자의 요청에 의해 작업이 중간에 중단되었습니다. 모든 결과가 저장되지 않은 점 주의하시기 바랍니다.'
# except Exception as e:
# message = '에러로 인해 작업이 중간에 중단되었습니다. 모든 결과가 저장되지 않은 점 주의하시기 바랍니다.'
finally:
if message:
print(message)
finally_callback()
if __name__ == '__main__':
parsed = parse_args()
task = DropboxSharedLinkTask(
access_token=os.environ.get('dropbox_access_token'),
shared_link_url=parsed.link,
limit=parsed.file_limit
)
def tree_callback():
task.retrieve_tree(parsed.input)
def finish_task():
task.export_entries(parsed.output)
if parsed.command == 'tree':
interruptable_task(tree_callback, finish_task)
elif parsed.command == 'merge':
task.merge(parsed.input, parsed.incremental, parsed.output)
elif parsed.command == 'aria2':
task.aria2_input_file(parsed.input, parsed.output)
@chwnam
Copy link
Author

chwnam commented Apr 16, 2018

세팅은 이렇습니다.

  • Python 2.7.14로 작성하였습니다.
  • pip install dropbox 로 드롭박스 패키지를 설치합니다.
  • 드롭박스 개발자 페이지로 가서 oauth2 access token을 발급받습니다.
  • 발급받은 토큰을 'dropbox_access_token'이란 환경 변수로 지정해 둡니다.

간단한 사용법입니다.

  1. 목록 생성하기
    python dropbox_attachments.py --command=tree --output='url_list.csv' --link='https://dropbox.com/.....'
    해당 공유 폴더를 재귀적으로 돌면서 모든 파일의 목록을 조사해 url_list.csv 파일로 추출합니다.

  2. 증분 목록 생성하기
    python dropbox_attachments.py --commend=tree --input='url_list.csv' --output='updated.csv' --link='https://dropbox.com/.....'
    해당 공유 폴더를 재귀적으로 도는 것까지는 같으나, input으로 준 파일과 대조하여, input에 없는 내용, 혹은 리비전이 변경된 (즉 더 새로운 파일) 파일만을 추출해 updated.csv 로 추출합니다.

  3. 기본/증분 목록 합치기
    python dropbox_attachments.py --commend=merge --input='url_list.csv' --incremental 'updated.csv' --output='new_url_list.csv' --link='https://dropbox.com/.....'
    기존의 url_list.csv 파일과 update.csv 파일을 합쳐 updated의 최신 내용을 url_list와 합칩니다. 결과는 new_url_list.csv로 저장합니다.

  4. aria2c 다운로드 유틸리티를 위한 input file 생성하기
    다운로드는 전문 다운로드 유틸리티에게 맡기는 것이 더욱 효율적입니다. aria2가 목록을 직접 다운로드 할 수 있도록 csv 파일을 가공하여 input-file 파라미터로 사용할 파일을 생성하도록 도와줍니다.
    python dropbox_attachments.py --command=aria2 --input 'url_list.csv' --output='aria2.txt' --link='https://dropbox.com/.....'
    이렇게 만들어진 aria2.txt는 다음처럼 사용 가능합니다.

aria2c --input-file=aria2.txt --dir='./downloads'

@chwnam
Copy link
Author

chwnam commented Apr 16, 2018

CSV 파일의 칼럼 정보입니다.

  • path: 공유 URL로부터의 경로입니다.
  • id: 내부적으로 사용하는 이 파일에 대한 유일한 식별자입니다.
  • name: 파일 이름, 디렉토리 이름입니다.
  • rev: 리비전 정보입니다. 이 문자열이 다르다면 같은 파일이 업데이트 된 것입니다.
  • size: 파일의 크기
  • content_hash: 파일의 해시. 해시 정보는 https://www.dropbox.com/developers/reference/content-hash 여기를 참고하세요.
  • url: 사용하지 않습니다.

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