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 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.timeout = httpx.Timeout(10.0, connect=60.0)
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
- async with httpx.AsyncClient(timeout=self.timeout) as client:
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
- head_response = await client.head(url)
170
+ # HEADリクエスト(オプション)
171
+ if not self.skip_head:
172
+ try:
173
+ head_response = await client.head(url)
66
174
 
67
- if head_response.status_code == 404:
68
- print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
69
- return None
175
+ if head_response.status_code == 404:
176
+ print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
177
+ return None
70
178
 
71
- if head_response.status_code != 200:
72
- print(f"エラー: ステータスコード {head_response.status_code}", file=sys.stderr)
73
- return None
179
+ if head_response.status_code != 200:
180
+ print(f"エラー: ステータスコード {head_response.status_code}", file=sys.stderr)
181
+ return None
74
182
 
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
- # 安全なファイル名を抽出(URLエンコードの解除)
81
- filename_part = cd.split("filename=")[1].strip('"')
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 not output:
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
- with httpx.Client(timeout=self.timeout) as client:
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
- head_response = client.head(url)
293
+ # HEADリクエスト(オプション)
294
+ if not self.skip_head:
295
+ try:
296
+ head_response = client.head(url)
174
297
 
175
- if head_response.status_code == 404:
176
- print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
177
- return None
298
+ if head_response.status_code == 404:
299
+ print(f"エラー: ID '{file_id}' のファイルが見つかりませんでした", file=sys.stderr)
300
+ return None
178
301
 
179
- if head_response.status_code != 200:
180
- print(f"エラー: ステータスコード {head_response.status_code}", file=sys.stderr)
181
- return None
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
- # Content-Dispositionヘッダーからファイル名を取得
184
- original_filename = None
185
- if "content-disposition" in head_response.headers:
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 not output:
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
- client = QDown(server_url=args.server, quiet=args.quiet)
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()
@@ -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
- ```
@@ -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