qdown 1.0.5__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qdown/qdown.py +254 -94
- qdown-1.1.0.dist-info/METADATA +135 -0
- qdown-1.1.0.dist-info/RECORD +9 -0
- qdown/gdown.py +0 -187
- qdown-1.0.5.dist-info/METADATA +0 -72
- qdown-1.0.5.dist-info/RECORD +0 -10
- {qdown-1.0.5.dist-info → qdown-1.1.0.dist-info}/WHEEL +0 -0
- {qdown-1.0.5.dist-info → qdown-1.1.0.dist-info}/entry_points.txt +0 -0
- {qdown-1.0.5.dist-info → qdown-1.1.0.dist-info}/top_level.txt +0 -0
qdown/qdown.py
CHANGED
@@ -9,12 +9,20 @@ qdown - Client for QualitegDrive
|
|
9
9
|
-o DIR 出力ディレクトリを指定
|
10
10
|
-s SERVER サーバーURLを指定 (デフォルト: https://drive.qualiteg.com)
|
11
11
|
-q, --quiet 進捗表示を非表示
|
12
|
+
--use-head HEADリクエストを使用(従来動作)
|
13
|
+
--skip-check 存在確認をスキップ(最速ダウンロード)
|
12
14
|
-h, --help ヘルプを表示
|
15
|
+
|
16
|
+
v1.1.0- の特徴:
|
17
|
+
- デフォルトでHEADリクエストをスキップ(大容量ファイル対応)
|
18
|
+
- /file/{id} での存在確認(より確実)
|
19
|
+
- 存在確認のスキップオプション(最速ダウンロード)
|
13
20
|
"""
|
14
21
|
|
15
22
|
import httpx
|
16
23
|
import os
|
17
24
|
import sys
|
25
|
+
import re
|
18
26
|
import argparse
|
19
27
|
import asyncio
|
20
28
|
import urllib.parse
|
@@ -24,24 +32,104 @@ from tqdm import tqdm
|
|
24
32
|
|
25
33
|
class QDown:
|
26
34
|
"""
|
27
|
-
ID認証付きファイルサーバー用のPython
|
35
|
+
ID認証付きファイルサーバー用のPythonクライアント(改良版)
|
28
36
|
"""
|
29
37
|
|
30
|
-
def __init__(self, server_url="https://drive.qualiteg.com", quiet=False
|
38
|
+
def __init__(self, server_url="https://drive.qualiteg.com", quiet=False,
|
39
|
+
skip_head=True, skip_exists_check=False):
|
31
40
|
"""
|
32
41
|
クライアントの初期化
|
33
42
|
|
34
43
|
Args:
|
35
44
|
server_url (str): ファイルサーバーのベースURL
|
36
45
|
quiet (bool): 進捗表示を非表示にするかどうか
|
46
|
+
skip_head (bool): HEADリクエストをスキップするかどうか(デフォルト: True)
|
47
|
+
skip_exists_check (bool): 存在確認をスキップするかどうか(デフォルト: False)
|
37
48
|
"""
|
38
49
|
self.server_url = server_url.rstrip('/')
|
39
50
|
self.quiet = quiet
|
40
|
-
self.
|
51
|
+
self.skip_head = skip_head
|
52
|
+
self.skip_exists_check = skip_exists_check
|
53
|
+
self.timeout = httpx.Timeout(30.0, connect=60.0, read=60.0)
|
54
|
+
|
55
|
+
def check_file_exists_via_page(self, file_id):
|
56
|
+
"""
|
57
|
+
/file/{id} ページで存在確認と基本情報取得
|
58
|
+
|
59
|
+
Args:
|
60
|
+
file_id (str): ファイルID
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
dict: ファイル情報(存在しない場合はNone)
|
64
|
+
"""
|
65
|
+
url = f"{self.server_url}/file/{file_id}"
|
66
|
+
|
67
|
+
try:
|
68
|
+
with httpx.Client(timeout=httpx.Timeout(10.0)) as client:
|
69
|
+
response = client.get(url)
|
70
|
+
|
71
|
+
if response.status_code == 404:
|
72
|
+
return None
|
73
|
+
|
74
|
+
if response.status_code == 200:
|
75
|
+
# ファイル名を抽出(オプション)
|
76
|
+
filename_match = re.search(r'<h3 class="mb-0">(.*?)</h3>', response.text)
|
77
|
+
filename = filename_match.group(1) if filename_match else None
|
78
|
+
|
79
|
+
# ファイルサイズを抽出(オプション)
|
80
|
+
size_match = re.search(r'<i class="fas fa-weight-hanging.*?</i>\s*([\d.]+ [KMGT]?B)', response.text)
|
81
|
+
file_size = size_match.group(1) if size_match else None
|
82
|
+
|
83
|
+
return {
|
84
|
+
'exists': True,
|
85
|
+
'filename': filename,
|
86
|
+
'file_size_display': file_size
|
87
|
+
}
|
88
|
+
|
89
|
+
# その他のステータスコード
|
90
|
+
return {'exists': False, 'status': response.status_code}
|
91
|
+
|
92
|
+
except Exception as e:
|
93
|
+
if not self.quiet:
|
94
|
+
print(f"警告: 存在確認中にエラーが発生しました: {e}", file=sys.stderr)
|
95
|
+
return None
|
96
|
+
|
97
|
+
async def check_file_exists_via_page_async(self, file_id):
|
98
|
+
"""
|
99
|
+
/file/{id} ページで存在確認と基本情報取得(非同期版)
|
100
|
+
"""
|
101
|
+
url = f"{self.server_url}/file/{file_id}"
|
102
|
+
|
103
|
+
try:
|
104
|
+
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
|
105
|
+
response = await client.get(url)
|
106
|
+
|
107
|
+
if response.status_code == 404:
|
108
|
+
return None
|
109
|
+
|
110
|
+
if response.status_code == 200:
|
111
|
+
filename_match = re.search(r'<h3 class="mb-0">(.*?)</h3>', response.text)
|
112
|
+
filename = filename_match.group(1) if filename_match else None
|
113
|
+
|
114
|
+
size_match = re.search(r'<i class="fas fa-weight-hanging.*?</i>\s*([\d.]+ [KMGT]?B)', response.text)
|
115
|
+
file_size = size_match.group(1) if size_match else None
|
116
|
+
|
117
|
+
return {
|
118
|
+
'exists': True,
|
119
|
+
'filename': filename,
|
120
|
+
'file_size_display': file_size
|
121
|
+
}
|
122
|
+
|
123
|
+
return {'exists': False, 'status': response.status_code}
|
124
|
+
|
125
|
+
except Exception as e:
|
126
|
+
if not self.quiet:
|
127
|
+
print(f"警告: 存在確認中にエラーが発生しました: {e}", file=sys.stderr)
|
128
|
+
return None
|
41
129
|
|
42
130
|
async def download_by_file_id(self, file_id, output=None, output_dir=None):
|
43
131
|
"""
|
44
|
-
ファイルID
|
132
|
+
ファイルIDを指定してファイルをダウンロード(非同期版)
|
45
133
|
|
46
134
|
Args:
|
47
135
|
file_id (str): ダウンロードするファイルのID (qd_id)
|
@@ -52,6 +140,7 @@ class QDown:
|
|
52
140
|
str: ダウンロードしたファイルのパス
|
53
141
|
"""
|
54
142
|
url = f"{self.server_url}/download/{file_id}"
|
143
|
+
suggested_filename = None
|
55
144
|
|
56
145
|
# 出力ディレクトリの設定
|
57
146
|
if output_dir:
|
@@ -59,63 +148,78 @@ class QDown:
|
|
59
148
|
else:
|
60
149
|
output_dir = "."
|
61
150
|
|
62
|
-
|
63
|
-
|
151
|
+
# 存在確認(スキップ可能)
|
152
|
+
if not self.skip_exists_check:
|
153
|
+
if not self.quiet:
|
154
|
+
print(f"[qdown] ファイル存在確認中: {file_id}")
|
155
|
+
|
156
|
+
file_info = await self.check_file_exists_via_page_async(file_id)
|
157
|
+
|
158
|
+
if file_info is None or not file_info.get('exists', False):
|
159
|
+
print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
|
160
|
+
return None
|
161
|
+
|
162
|
+
suggested_filename = file_info.get('filename')
|
163
|
+
if not self.quiet and suggested_filename:
|
164
|
+
print(f"[qdown] ファイル検出: {suggested_filename}")
|
165
|
+
if file_info.get('file_size_display'):
|
166
|
+
print(f"[qdown] サイズ: {file_info['file_size_display']}")
|
167
|
+
|
168
|
+
async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=True) as client:
|
64
169
|
try:
|
65
|
-
|
170
|
+
# HEADリクエスト(オプション)
|
171
|
+
if not self.skip_head:
|
172
|
+
try:
|
173
|
+
head_response = await client.head(url)
|
66
174
|
|
67
|
-
|
68
|
-
|
69
|
-
|
175
|
+
if head_response.status_code == 404:
|
176
|
+
print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
|
177
|
+
return None
|
70
178
|
|
71
|
-
|
72
|
-
|
73
|
-
|
179
|
+
if head_response.status_code != 200:
|
180
|
+
print(f"エラー: ステータスコード {head_response.status_code}", file=sys.stderr)
|
181
|
+
return None
|
74
182
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
# filename*=UTF-8 形式のエンコードがある場合
|
84
|
-
if "filename*=UTF-8''" in cd:
|
85
|
-
encoded_part = cd.split("filename*=UTF-8''")[1]
|
86
|
-
# セミコロンやダブルクォートがあれば処理
|
87
|
-
if '"' in encoded_part:
|
88
|
-
encoded_part = encoded_part.split('"')[0]
|
89
|
-
if ';' in encoded_part:
|
90
|
-
encoded_part = encoded_part.split(';')[0]
|
91
|
-
# URLデコードして正しいファイル名を取得
|
92
|
-
original_filename = urllib.parse.unquote(encoded_part)
|
93
|
-
else:
|
94
|
-
# 通常のファイル名(エスケープ処理)
|
95
|
-
original_filename = filename_part.replace('"', '').split(';')[0]
|
183
|
+
# Content-Dispositionヘッダーからファイル名を取得
|
184
|
+
if "content-disposition" in head_response.headers and not suggested_filename:
|
185
|
+
cd = head_response.headers["content-disposition"]
|
186
|
+
suggested_filename = self._extract_filename_from_header(cd)
|
187
|
+
|
188
|
+
except httpx.TimeoutException:
|
189
|
+
if not self.quiet:
|
190
|
+
print(f"警告: HEADリクエストがタイムアウトしました。ダウンロードを続行します。", file=sys.stderr)
|
96
191
|
|
97
192
|
# 保存用のファイル名を決定
|
98
|
-
if
|
99
|
-
if original_filename:
|
100
|
-
# パスとして安全なファイル名に変換
|
101
|
-
safe_filename = os.path.basename(original_filename)
|
102
|
-
output_filename = safe_filename
|
103
|
-
else:
|
104
|
-
output_filename = f"download_{file_id}"
|
105
|
-
else:
|
193
|
+
if output:
|
106
194
|
output_filename = output
|
195
|
+
elif suggested_filename:
|
196
|
+
output_filename = os.path.basename(suggested_filename)
|
197
|
+
else:
|
198
|
+
output_filename = f"download_{file_id}"
|
107
199
|
|
108
200
|
file_path = os.path.join(output_dir, output_filename)
|
109
201
|
|
110
|
-
#
|
111
|
-
total_size = int(head_response.headers.get("content-length", 0))
|
112
|
-
|
113
|
-
# ストリーミングダウンロードを開始
|
202
|
+
# ストリーミングダウンロード
|
114
203
|
async with client.stream("GET", url) as response:
|
204
|
+
if response.status_code == 404:
|
205
|
+
print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
|
206
|
+
return None
|
207
|
+
|
115
208
|
if response.status_code != 200:
|
116
209
|
print(f"エラー: ダウンロード中にエラーが発生しました。ステータスコード: {response.status_code}", file=sys.stderr)
|
117
210
|
return None
|
118
211
|
|
212
|
+
# Content-Dispositionヘッダーから最終的なファイル名を取得
|
213
|
+
if "content-disposition" in response.headers and not output:
|
214
|
+
cd = response.headers["content-disposition"]
|
215
|
+
extracted_name = self._extract_filename_from_header(cd)
|
216
|
+
if extracted_name:
|
217
|
+
output_filename = os.path.basename(extracted_name)
|
218
|
+
file_path = os.path.join(output_dir, output_filename)
|
219
|
+
|
220
|
+
# ファイルサイズを取得
|
221
|
+
total_size = int(response.headers.get("content-length", 0))
|
222
|
+
|
119
223
|
with open(file_path, "wb") as f:
|
120
224
|
if not self.quiet and total_size > 0:
|
121
225
|
progress_bar = tqdm(
|
@@ -126,8 +230,7 @@ class QDown:
|
|
126
230
|
)
|
127
231
|
|
128
232
|
downloaded = 0
|
129
|
-
|
130
|
-
async for chunk in response.aiter_bytes():
|
233
|
+
async for chunk in response.aiter_bytes(chunk_size=8192):
|
131
234
|
f.write(chunk)
|
132
235
|
if not self.quiet and total_size > 0:
|
133
236
|
downloaded += len(chunk)
|
@@ -160,6 +263,7 @@ class QDown:
|
|
160
263
|
str: ダウンロードしたファイルのパス
|
161
264
|
"""
|
162
265
|
url = f"{self.server_url}/download/{file_id}"
|
266
|
+
suggested_filename = None
|
163
267
|
|
164
268
|
# 出力ディレクトリの設定
|
165
269
|
if output_dir:
|
@@ -167,63 +271,78 @@ class QDown:
|
|
167
271
|
else:
|
168
272
|
output_dir = "."
|
169
273
|
|
170
|
-
|
171
|
-
|
274
|
+
# 存在確認(スキップ可能)
|
275
|
+
if not self.skip_exists_check:
|
276
|
+
if not self.quiet:
|
277
|
+
print(f"[qdown] ファイル存在確認中: {file_id}")
|
278
|
+
|
279
|
+
file_info = self.check_file_exists_via_page(file_id)
|
280
|
+
|
281
|
+
if file_info is None or not file_info.get('exists', False):
|
282
|
+
print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
|
283
|
+
return None
|
284
|
+
|
285
|
+
suggested_filename = file_info.get('filename')
|
286
|
+
if not self.quiet and suggested_filename:
|
287
|
+
print(f"[qdown] ファイル検出: {suggested_filename}")
|
288
|
+
if file_info.get('file_size_display'):
|
289
|
+
print(f"[qdown] サイズ: {file_info['file_size_display']}")
|
290
|
+
|
291
|
+
with httpx.Client(timeout=self.timeout, follow_redirects=True) as client:
|
172
292
|
try:
|
173
|
-
|
293
|
+
# HEADリクエスト(オプション)
|
294
|
+
if not self.skip_head:
|
295
|
+
try:
|
296
|
+
head_response = client.head(url)
|
174
297
|
|
175
|
-
|
176
|
-
|
177
|
-
|
298
|
+
if head_response.status_code == 404:
|
299
|
+
print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
|
300
|
+
return None
|
178
301
|
|
179
|
-
|
180
|
-
|
181
|
-
|
302
|
+
if head_response.status_code != 200:
|
303
|
+
print(f"エラー: ステータスコード {head_response.status_code}", file=sys.stderr)
|
304
|
+
return None
|
305
|
+
|
306
|
+
# Content-Dispositionヘッダーからファイル名を取得
|
307
|
+
if "content-disposition" in head_response.headers and not suggested_filename:
|
308
|
+
cd = head_response.headers["content-disposition"]
|
309
|
+
suggested_filename = self._extract_filename_from_header(cd)
|
182
310
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
cd = head_response.headers["content-disposition"]
|
187
|
-
if "filename=" in cd:
|
188
|
-
# 安全なファイル名を抽出(URLエンコードの解除)
|
189
|
-
filename_part = cd.split("filename=")[1].strip('"')
|
190
|
-
|
191
|
-
# filename*=UTF-8 形式のエンコードがある場合
|
192
|
-
if "filename*=UTF-8''" in cd:
|
193
|
-
encoded_part = cd.split("filename*=UTF-8''")[1]
|
194
|
-
# セミコロンやダブルクォートがあれば処理
|
195
|
-
if '"' in encoded_part:
|
196
|
-
encoded_part = encoded_part.split('"')[0]
|
197
|
-
if ';' in encoded_part:
|
198
|
-
encoded_part = encoded_part.split(';')[0]
|
199
|
-
# URLデコードして正しいファイル名を取得
|
200
|
-
original_filename = urllib.parse.unquote(encoded_part)
|
201
|
-
else:
|
202
|
-
# 通常のファイル名(エスケープ処理)
|
203
|
-
original_filename = filename_part.replace('"', '').split(';')[0]
|
311
|
+
except httpx.TimeoutException:
|
312
|
+
if not self.quiet:
|
313
|
+
print(f"警告: HEADリクエストがタイムアウトしました。ダウンロードを続行します。", file=sys.stderr)
|
204
314
|
|
205
315
|
# 保存用のファイル名を決定
|
206
|
-
if
|
207
|
-
if original_filename:
|
208
|
-
# パスとして安全なファイル名に変換
|
209
|
-
safe_filename = os.path.basename(original_filename)
|
210
|
-
output_filename = safe_filename
|
211
|
-
else:
|
212
|
-
output_filename = f"download_{file_id}"
|
213
|
-
else:
|
316
|
+
if output:
|
214
317
|
output_filename = output
|
318
|
+
elif suggested_filename:
|
319
|
+
output_filename = os.path.basename(suggested_filename)
|
320
|
+
else:
|
321
|
+
output_filename = f"download_{file_id}"
|
215
322
|
|
216
323
|
file_path = os.path.join(output_dir, output_filename)
|
217
324
|
|
218
|
-
#
|
219
|
-
total_size = int(head_response.headers.get("content-length", 0))
|
220
|
-
|
221
|
-
# ストリーミングダウンロードを開始
|
325
|
+
# ストリーミングダウンロード
|
222
326
|
with client.stream("GET", url) as response:
|
327
|
+
if response.status_code == 404:
|
328
|
+
print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
|
329
|
+
return None
|
330
|
+
|
223
331
|
if response.status_code != 200:
|
224
332
|
print(f"エラー: ダウンロード中にエラーが発生しました。ステータスコード: {response.status_code}", file=sys.stderr)
|
225
333
|
return None
|
226
334
|
|
335
|
+
# Content-Dispositionヘッダーから最終的なファイル名を取得
|
336
|
+
if "content-disposition" in response.headers and not output:
|
337
|
+
cd = response.headers["content-disposition"]
|
338
|
+
extracted_name = self._extract_filename_from_header(cd)
|
339
|
+
if extracted_name:
|
340
|
+
output_filename = os.path.basename(extracted_name)
|
341
|
+
file_path = os.path.join(output_dir, output_filename)
|
342
|
+
|
343
|
+
# ファイルサイズを取得
|
344
|
+
total_size = int(response.headers.get("content-length", 0))
|
345
|
+
|
227
346
|
with open(file_path, "wb") as f:
|
228
347
|
if not self.quiet and total_size > 0:
|
229
348
|
progress_bar = tqdm(
|
@@ -234,8 +353,7 @@ class QDown:
|
|
234
353
|
)
|
235
354
|
|
236
355
|
downloaded = 0
|
237
|
-
|
238
|
-
for chunk in response.iter_bytes():
|
356
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
239
357
|
f.write(chunk)
|
240
358
|
if not self.quiet and total_size > 0:
|
241
359
|
downloaded += len(chunk)
|
@@ -273,6 +391,37 @@ class QDown:
|
|
273
391
|
print(f"エラー: {e}", file=sys.stderr)
|
274
392
|
return None
|
275
393
|
|
394
|
+
def _extract_filename_from_header(self, content_disposition):
|
395
|
+
"""
|
396
|
+
Content-Dispositionヘッダーからファイル名を抽出
|
397
|
+
|
398
|
+
Args:
|
399
|
+
content_disposition (str): Content-Dispositionヘッダーの値
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
str: 抽出されたファイル名(抽出できない場合はNone)
|
403
|
+
"""
|
404
|
+
if not content_disposition or "filename=" not in content_disposition:
|
405
|
+
return None
|
406
|
+
|
407
|
+
try:
|
408
|
+
# filename*=UTF-8''形式のエンコードがある場合
|
409
|
+
if "filename*=UTF-8''" in content_disposition:
|
410
|
+
encoded_part = content_disposition.split("filename*=UTF-8''")[1]
|
411
|
+
# セミコロンやダブルクォートがあれば処理
|
412
|
+
if '"' in encoded_part:
|
413
|
+
encoded_part = encoded_part.split('"')[0]
|
414
|
+
if ';' in encoded_part:
|
415
|
+
encoded_part = encoded_part.split(';')[0]
|
416
|
+
# URLデコードして正しいファイル名を取得
|
417
|
+
return urllib.parse.unquote(encoded_part)
|
418
|
+
else:
|
419
|
+
# 通常のファイル名
|
420
|
+
filename_part = content_disposition.split("filename=")[1].strip('"')
|
421
|
+
return filename_part.replace('"', '').split(';')[0]
|
422
|
+
except Exception:
|
423
|
+
return None
|
424
|
+
|
276
425
|
|
277
426
|
def main():
|
278
427
|
parser = argparse.ArgumentParser(
|
@@ -285,6 +434,8 @@ def main():
|
|
285
434
|
parser.add_argument("-o", dest="output_dir", help="出力ディレクトリ")
|
286
435
|
parser.add_argument("-s", dest="server", default="https://drive.qualiteg.com", help="サーバーURL")
|
287
436
|
parser.add_argument("-q", "--quiet", action="store_true", help="進捗表示を非表示")
|
437
|
+
parser.add_argument("--use-head", action="store_true", help="HEADリクエストを使用(従来動作)")
|
438
|
+
parser.add_argument("--skip-check", action="store_true", help="存在確認をスキップ(最速ダウンロード)")
|
288
439
|
parser.add_argument("-h", "--help", action="store_true", help="ヘルプを表示")
|
289
440
|
|
290
441
|
args = parser.parse_args()
|
@@ -293,7 +444,16 @@ def main():
|
|
293
444
|
print(__doc__)
|
294
445
|
sys.exit(0)
|
295
446
|
|
296
|
-
|
447
|
+
# skip_headのデフォルトはTrue(HEADリクエストをスキップ)
|
448
|
+
# --use-headが指定された場合のみFalse(HEADリクエストを使用)
|
449
|
+
skip_head = not args.use_head
|
450
|
+
|
451
|
+
client = QDown(
|
452
|
+
server_url=args.server,
|
453
|
+
quiet=args.quiet,
|
454
|
+
skip_head=skip_head,
|
455
|
+
skip_exists_check=args.skip_check
|
456
|
+
)
|
297
457
|
|
298
458
|
result = asyncio.run(client.download_by_file_id(
|
299
459
|
file_id=args.id,
|
@@ -308,4 +468,4 @@ def main():
|
|
308
468
|
|
309
469
|
|
310
470
|
if __name__ == "__main__":
|
311
|
-
main()
|
471
|
+
main()
|
@@ -0,0 +1,135 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: qdown
|
3
|
+
Version: 1.1.0
|
4
|
+
Summary: Client for QualitegDrive
|
5
|
+
Home-page: https://github.com/qualiteg/qdown
|
6
|
+
Author: Qualiteg Inc.
|
7
|
+
Author-email: qualiteger@qualiteg.com
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
9
|
+
Classifier: Intended Audience :: Developers
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Classifier: Programming Language :: Python :: 3.7
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
16
|
+
Requires-Python: >=3.7
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
Requires-Dist: httpx (>=0.23.0)
|
19
|
+
Requires-Dist: tqdm (>=4.64.0)
|
20
|
+
|
21
|
+
# qdown
|
22
|
+
|
23
|
+
A Python client for downloading files from QualitegDrive operated by Qualiteg Inc.
|
24
|
+
|
25
|
+
[Japanese](README.ja.md)
|
26
|
+
|
27
|
+
## Install
|
28
|
+
|
29
|
+
```
|
30
|
+
pip install qdown
|
31
|
+
```
|
32
|
+
|
33
|
+
or
|
34
|
+
|
35
|
+
```
|
36
|
+
pip install git+https://github.com/qualiteg/qdown.git
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
```
|
42
|
+
qdown ID [options]
|
43
|
+
|
44
|
+
Options:
|
45
|
+
-O FILENAME Specify output filename
|
46
|
+
-o DIR Specify output directory
|
47
|
+
-s SERVER Specify server URL (default: https://drive.qualiteg.com)
|
48
|
+
-q, --quiet Hide progress display
|
49
|
+
--skip-check Skip file existence check (fastest download)
|
50
|
+
--use-head Use HEAD request (legacy behavior)
|
51
|
+
-h, --help Display help
|
52
|
+
```
|
53
|
+
|
54
|
+
### New Features (v1.1+)
|
55
|
+
|
56
|
+
Enhanced support for large files and improved performance:
|
57
|
+
|
58
|
+
**New Options:**
|
59
|
+
- `--skip-check`: Skip file existence verification for fastest download (recommended for large files)
|
60
|
+
- `--use-head`: Use HEAD request method (default is disabled to prevent timeouts)
|
61
|
+
|
62
|
+
**Improvements:**
|
63
|
+
- HEAD requests are now skipped by default (resolves timeout issues with large files)
|
64
|
+
- File existence verification via `/file/{id}` endpoint (more reliable)
|
65
|
+
|
66
|
+
## Download Examples
|
67
|
+
|
68
|
+
### Example 1: Basic Download
|
69
|
+
```
|
70
|
+
qdown xxxxxxxxxxxxx -O my_file.txt
|
71
|
+
```
|
72
|
+
|
73
|
+
### Example 2: From Your Original HTTP Server
|
74
|
+
```
|
75
|
+
qdown xxxxxxxxxxxxx -O my_file.txt -s http://host.docker.internal:3000
|
76
|
+
```
|
77
|
+
|
78
|
+
### Example 3: Fast Download for Large Files (v1.1+)
|
79
|
+
```
|
80
|
+
# Skip all checks for maximum speed
|
81
|
+
qdown xxxxxxxxxxxxx -O large_file.zip --skip-check
|
82
|
+
|
83
|
+
# Silent mode with skip check
|
84
|
+
qdown xxxxxxxxxxxxx -O huge_file.zip --skip-check -q
|
85
|
+
```
|
86
|
+
|
87
|
+
### Example 4: Legacy Behavior (v1.1+)
|
88
|
+
```
|
89
|
+
# Use traditional HEAD request (if needed for compatibility)
|
90
|
+
qdown xxxxxxxxxxxxx -O file.txt --use-head
|
91
|
+
```
|
92
|
+
|
93
|
+
## Python API
|
94
|
+
|
95
|
+
```python
|
96
|
+
import qdown
|
97
|
+
|
98
|
+
# Basic download
|
99
|
+
file_path = qdown.download("file_id_here")
|
100
|
+
|
101
|
+
# Download with output filename
|
102
|
+
file_path = qdown.download("file_id_here", output_path="my_file.txt")
|
103
|
+
|
104
|
+
# Download from custom server
|
105
|
+
file_path = qdown.download(
|
106
|
+
"file_id_here",
|
107
|
+
output_path="my_file.txt",
|
108
|
+
server_url="http://localhost:3000"
|
109
|
+
)
|
110
|
+
|
111
|
+
# Fast download for large files (v1.1+)
|
112
|
+
file_path = qdown.download(
|
113
|
+
"file_id_here",
|
114
|
+
output_path="large_file.zip",
|
115
|
+
skip_exists_check=True, # Skip existence check
|
116
|
+
skip_head=True, # Skip HEAD request (default)
|
117
|
+
quiet=True # Silent mode
|
118
|
+
)
|
119
|
+
|
120
|
+
# Use legacy behavior (v1.1+)
|
121
|
+
file_path = qdown.download(
|
122
|
+
"file_id_here",
|
123
|
+
skip_head=False # Enable HEAD request
|
124
|
+
)
|
125
|
+
```
|
126
|
+
|
127
|
+
### New Parameters (v1.1+)
|
128
|
+
- `skip_exists_check`: Skip file existence verification (default: False)
|
129
|
+
- `skip_head`: Skip HEAD request (default: True)
|
130
|
+
|
131
|
+
## Uninstall
|
132
|
+
|
133
|
+
```
|
134
|
+
pip uninstall qdown -y
|
135
|
+
```
|
@@ -0,0 +1,9 @@
|
|
1
|
+
qdown/__init__.py,sha256=rNxBW4Zo6nfWYN2JnLwJVpyGEX3RtBjoE7QlSy0FcxU,2975
|
2
|
+
qdown/qdown.py,sha256=K5bRiz2_jJrSGhA4xVpepPAHFvynXt5esoqdyUVpImE,20984
|
3
|
+
z_examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
z_examples/example.py,sha256=3FQoW22Frgyx5ulb7JOgwgcEBd8K3FI_BuFZP2E54ck,137
|
5
|
+
qdown-1.1.0.dist-info/METADATA,sha256=AEkpjWkeVZ3E0A1e_GSUAsMHRcy1e4K3KaT_S6AkLKk,3486
|
6
|
+
qdown-1.1.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
7
|
+
qdown-1.1.0.dist-info/entry_points.txt,sha256=4uunDwX_8iGbNA0DKwggOftuKUXvoHxGzvXUd2SW9LM,43
|
8
|
+
qdown-1.1.0.dist-info/top_level.txt,sha256=eVEHrbec1mx2PWv03GzKwFTbdvQqFOAps3GuveF2Ap8,17
|
9
|
+
qdown-1.1.0.dist-info/RECORD,,
|
qdown/gdown.py
DELETED
@@ -1,187 +0,0 @@
|
|
1
|
-
|
2
|
-
"""
|
3
|
-
qdown - Client for QualitegDrive
|
4
|
-
|
5
|
-
使用方法:
|
6
|
-
qdown ID [オプション]
|
7
|
-
|
8
|
-
オプション:
|
9
|
-
-O FILENAME 出力ファイル名を指定
|
10
|
-
-o DIR 出力ディレクトリを指定
|
11
|
-
-s SERVER サーバーURLを指定 (デフォルト: https://drive.qualiteg.com)
|
12
|
-
-q, --quiet 進捗表示を非表示
|
13
|
-
-h, --help ヘルプを表示
|
14
|
-
"""
|
15
|
-
|
16
|
-
import httpx
|
17
|
-
import os
|
18
|
-
import sys
|
19
|
-
import argparse
|
20
|
-
import asyncio
|
21
|
-
from pathlib import Path
|
22
|
-
from tqdm import tqdm
|
23
|
-
|
24
|
-
|
25
|
-
class QDown:
|
26
|
-
"""
|
27
|
-
ID認証付きファイルサーバー用のPythonクライアント
|
28
|
-
"""
|
29
|
-
|
30
|
-
def __init__(self, server_url="https://drive.qualiteg.com", quiet=False):
|
31
|
-
"""
|
32
|
-
クライアントの初期化
|
33
|
-
|
34
|
-
Args:
|
35
|
-
server_url (str): ファイルサーバーのベースURL
|
36
|
-
quiet (bool): 進捗表示を非表示にするかどうか
|
37
|
-
"""
|
38
|
-
self.server_url = server_url.rstrip('/')
|
39
|
-
self.quiet = quiet
|
40
|
-
self.timeout = httpx.Timeout(10.0, connect=60.0)
|
41
|
-
|
42
|
-
async def download(self, file_id, output=None, output_dir=None):
|
43
|
-
"""
|
44
|
-
ファイルIDを指定してファイルをダウンロード
|
45
|
-
|
46
|
-
Args:
|
47
|
-
file_id (str): ダウンロードするファイルのID (qd_id)
|
48
|
-
output (str, optional): 出力ファイル名
|
49
|
-
output_dir (str, optional): 出力ディレクトリ
|
50
|
-
|
51
|
-
Returns:
|
52
|
-
str: ダウンロードしたファイルのパス
|
53
|
-
"""
|
54
|
-
url = f"{self.server_url}/download/{file_id}"
|
55
|
-
|
56
|
-
# 出力ディレクトリの設定
|
57
|
-
if output_dir:
|
58
|
-
os.makedirs(output_dir, exist_ok=True)
|
59
|
-
else:
|
60
|
-
output_dir = "."
|
61
|
-
|
62
|
-
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
63
|
-
# まず、ヘッド要求を送信してファイル情報を取得
|
64
|
-
try:
|
65
|
-
head_response = await client.head(url)
|
66
|
-
|
67
|
-
if head_response.status_code == 404:
|
68
|
-
print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
|
69
|
-
return None
|
70
|
-
|
71
|
-
if head_response.status_code != 200:
|
72
|
-
print(f"エラー: ステータスコード {head_response.status_code}", file=sys.stderr)
|
73
|
-
return None
|
74
|
-
|
75
|
-
# Content-Dispositionヘッダーからファイル名を取得
|
76
|
-
original_filename = None
|
77
|
-
if "content-disposition" in head_response.headers:
|
78
|
-
cd = head_response.headers["content-disposition"]
|
79
|
-
if "filename=" in cd:
|
80
|
-
original_filename = cd.split("filename=")[1].strip('"')
|
81
|
-
|
82
|
-
# 保存用のファイル名を決定
|
83
|
-
if not output:
|
84
|
-
if original_filename:
|
85
|
-
output_filename = original_filename
|
86
|
-
else:
|
87
|
-
output_filename = f"download_{file_id}"
|
88
|
-
else:
|
89
|
-
output_filename = output
|
90
|
-
|
91
|
-
file_path = os.path.join(output_dir, output_filename)
|
92
|
-
|
93
|
-
# ファイルサイズを取得(プログレスバー用)
|
94
|
-
total_size = int(head_response.headers.get("content-length", 0))
|
95
|
-
|
96
|
-
# ストリーミングダウンロードを開始
|
97
|
-
async with client.stream("GET", url) as response:
|
98
|
-
if response.status_code != 200:
|
99
|
-
print(f"エラー: ダウンロード中にエラーが発生しました。ステータスコード: {response.status_code}", file=sys.stderr)
|
100
|
-
return None
|
101
|
-
|
102
|
-
with open(file_path, "wb") as f:
|
103
|
-
if not self.quiet and total_size > 0:
|
104
|
-
progress_bar = tqdm(
|
105
|
-
total=total_size,
|
106
|
-
unit="B",
|
107
|
-
unit_scale=True,
|
108
|
-
desc=f"ダウンロード中: {output_filename}"
|
109
|
-
)
|
110
|
-
|
111
|
-
downloaded = 0
|
112
|
-
|
113
|
-
async for chunk in response.aiter_bytes():
|
114
|
-
f.write(chunk)
|
115
|
-
if not self.quiet and total_size > 0:
|
116
|
-
downloaded += len(chunk)
|
117
|
-
progress_bar.update(len(chunk))
|
118
|
-
|
119
|
-
if not self.quiet and total_size > 0:
|
120
|
-
progress_bar.close()
|
121
|
-
|
122
|
-
if not self.quiet:
|
123
|
-
print(f"[qdown] ファイルを保存しました: {file_path}")
|
124
|
-
return file_path
|
125
|
-
|
126
|
-
except httpx.RequestError as e:
|
127
|
-
print(f"エラー: リクエストに失敗しました - {e}", file=sys.stderr)
|
128
|
-
# Add this to the except httpx.RequestError block
|
129
|
-
if "Name or service not known" in str(e):
|
130
|
-
print("WSLで実行しているとき、このDNSエラーに遭遇した場合は以下をお試しください")
|
131
|
-
print("If you are running this in WSL, please try setting up DNS as follows:", file=sys.stderr)
|
132
|
-
print("STEP 1: Disable the automatic DNS configuration in WSL2", file=sys.stderr)
|
133
|
-
print("In WSL bash, run the following to prevent automatic generation of resolv.conf:", file=sys.stderr)
|
134
|
-
print('sudo sh -c \'cat > /etc/wsl.conf << EOF', file=sys.stderr)
|
135
|
-
print('[user]', file=sys.stderr)
|
136
|
-
print('default=mlu', file=sys.stderr)
|
137
|
-
print('[network]', file=sys.stderr)
|
138
|
-
print('generateResolvConf = false', file=sys.stderr)
|
139
|
-
print('EOF\'', file=sys.stderr)
|
140
|
-
print("STEP 2: Restart WSL from Windows", file=sys.stderr)
|
141
|
-
print('wsl --shutdown', file=sys.stderr)
|
142
|
-
print("STEP 3: After restarting WSL, run the following in the shell:", file=sys.stderr)
|
143
|
-
print('sudo sh -c \'cat > /etc/resolv.conf << EOF', file=sys.stderr)
|
144
|
-
print('nameserver 8.8.8.8', file=sys.stderr)
|
145
|
-
print('nameserver 8.8.4.4', file=sys.stderr)
|
146
|
-
print('EOF\'', file=sys.stderr)
|
147
|
-
return None
|
148
|
-
except Exception as e:
|
149
|
-
print(f"エラー: {e}", file=sys.stderr)
|
150
|
-
return None
|
151
|
-
|
152
|
-
|
153
|
-
def main():
|
154
|
-
parser = argparse.ArgumentParser(
|
155
|
-
description="qdown - IDベースファイルダウンロードツール",
|
156
|
-
add_help=False
|
157
|
-
)
|
158
|
-
|
159
|
-
parser.add_argument("id", nargs="?", help="ダウンロードするファイルのID")
|
160
|
-
parser.add_argument("-O", dest="output", help="出力ファイル名")
|
161
|
-
parser.add_argument("-o", dest="output_dir", help="出力ディレクトリ")
|
162
|
-
parser.add_argument("-s", dest="server", default="https://drive.qualiteg.com", help="サーバーURL")
|
163
|
-
parser.add_argument("-q", "--quiet", action="store_true", help="進捗表示を非表示")
|
164
|
-
parser.add_argument("-h", "--help", action="store_true", help="ヘルプを表示")
|
165
|
-
|
166
|
-
args = parser.parse_args()
|
167
|
-
|
168
|
-
if args.help or not args.id:
|
169
|
-
print(__doc__)
|
170
|
-
sys.exit(0)
|
171
|
-
|
172
|
-
client = QDown(server_url=args.server, quiet=args.quiet)
|
173
|
-
|
174
|
-
result = asyncio.run(client.download(
|
175
|
-
file_id=args.id,
|
176
|
-
output=args.output,
|
177
|
-
output_dir=args.output_dir
|
178
|
-
))
|
179
|
-
|
180
|
-
if result:
|
181
|
-
sys.exit(0)
|
182
|
-
else:
|
183
|
-
sys.exit(1)
|
184
|
-
|
185
|
-
|
186
|
-
if __name__ == "__main__":
|
187
|
-
main()
|
qdown-1.0.5.dist-info/METADATA
DELETED
@@ -1,72 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: qdown
|
3
|
-
Version: 1.0.5
|
4
|
-
Summary: Client for QualitegDrive
|
5
|
-
Home-page: https://github.com/qualiteg/qdown
|
6
|
-
Author: Qualiteg Inc.
|
7
|
-
Author-email: qualiteger@qualiteg.com
|
8
|
-
Classifier: Development Status :: 3 - Alpha
|
9
|
-
Classifier: Intended Audience :: Developers
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
12
|
-
Classifier: Programming Language :: Python :: 3.7
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
16
|
-
Requires-Python: >=3.7
|
17
|
-
Description-Content-Type: text/markdown
|
18
|
-
Requires-Dist: httpx (>=0.23.0)
|
19
|
-
Requires-Dist: tqdm (>=4.64.0)
|
20
|
-
|
21
|
-
# qdown
|
22
|
-
|
23
|
-
A Python client for downloading files from QualitegDrive operated by Qualiteg Inc.
|
24
|
-
|
25
|
-
[Japanese](README.ja.md)
|
26
|
-
|
27
|
-
# install
|
28
|
-
|
29
|
-
```
|
30
|
-
pip install qdown
|
31
|
-
```
|
32
|
-
|
33
|
-
or
|
34
|
-
|
35
|
-
```
|
36
|
-
pip install git+https://github.com/qualiteg/qdown.git
|
37
|
-
```
|
38
|
-
|
39
|
-
# usage
|
40
|
-
|
41
|
-
```
|
42
|
-
|
43
|
-
qdown ID [options]
|
44
|
-
|
45
|
-
Options:
|
46
|
-
-O FILENAME Specify output filename
|
47
|
-
-o DIR Specify output directory
|
48
|
-
-s SERVER Specify server URL (default: https://drive.qualiteg.com)
|
49
|
-
-q, --quiet Hide progress display
|
50
|
-
-h, --help Display help
|
51
|
-
```
|
52
|
-
|
53
|
-
## download example1
|
54
|
-
|
55
|
-
```
|
56
|
-
qdown xxxxxxxxxxxxx -O my_file.txt
|
57
|
-
```
|
58
|
-
|
59
|
-
## download example2
|
60
|
-
|
61
|
-
From Your Original HTTP Server
|
62
|
-
|
63
|
-
```
|
64
|
-
qdown xxxxxxxxxxxxx -O my_file.txt -s http://host.docker.internal:3000
|
65
|
-
```
|
66
|
-
|
67
|
-
|
68
|
-
# uninstall
|
69
|
-
|
70
|
-
```
|
71
|
-
pip uninstall qdown -y
|
72
|
-
```
|
qdown-1.0.5.dist-info/RECORD
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
qdown/__init__.py,sha256=rNxBW4Zo6nfWYN2JnLwJVpyGEX3RtBjoE7QlSy0FcxU,2975
|
2
|
-
qdown/gdown.py,sha256=s1KuO9MzIVqHBCqISVoHTq2mI7taM7_FBbBMu_uxDQA,7920
|
3
|
-
qdown/qdown.py,sha256=56XTR3CqhlLPvb1T3xGCX5eG5ouL-2yUo2c1m4xhyrY,13867
|
4
|
-
z_examples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
z_examples/example.py,sha256=3FQoW22Frgyx5ulb7JOgwgcEBd8K3FI_BuFZP2E54ck,137
|
6
|
-
qdown-1.0.5.dist-info/METADATA,sha256=9YN1UyAxHJcJ125W206Ocrh0pMpMV0VFosiCg_NPVGY,1515
|
7
|
-
qdown-1.0.5.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
8
|
-
qdown-1.0.5.dist-info/entry_points.txt,sha256=4uunDwX_8iGbNA0DKwggOftuKUXvoHxGzvXUd2SW9LM,43
|
9
|
-
qdown-1.0.5.dist-info/top_level.txt,sha256=eVEHrbec1mx2PWv03GzKwFTbdvQqFOAps3GuveF2Ap8,17
|
10
|
-
qdown-1.0.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|