easyget 1.0.0__tar.gz

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.
easyget-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.2
2
+ Name: easyget
3
+ Version: 1.0.0
4
+ Summary: Fast, easy-to-use multi-platform file downloader
5
+ Author: Your Name
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Operating System :: OS Independent
8
+ Requires-Python: >=3.7
9
+ Requires-Dist: httpx>=0.27.0
10
+ Requires-Dist: tqdm>=4.60.0
11
+ Dynamic: author
12
+ Dynamic: classifier
13
+ Dynamic: requires-dist
14
+ Dynamic: requires-python
15
+ Dynamic: summary
@@ -0,0 +1,146 @@
1
+ # easyget
2
+
3
+ `easyget` is a wget/curl-compatible command-line downloader written in Python.
4
+ It supports modern features like multithreading, speed limits, resume support, wildcard URL expansion, and progress bars — all in a user-friendly bilingual (English + Korean) interface.
5
+
6
+ `easyget`는 Python으로 작성된 wget/curl 호환 명령줄 다운로드 도구입니다.
7
+ 멀티스레드, 속도 제한, 이어받기, 와일드카드 URL 확장, 진행률 표시 등 현대적인 기능을 지원하며, 영어와 한국어로 모두 친절하게 안내합니다.
8
+
9
+ ---
10
+
11
+ ## Features / 특징
12
+
13
+ - ✅ **wget/curl-style options** (`-O`, `-c`, `--limit-rate`)
14
+ - ✅ **Multithreaded downloads** (default 4 threads)
15
+ - ✅ **Speed limits** (e.g., `--max-speed 1M`)
16
+ - ✅ **Resume support** (`--resume` or `-c`)
17
+ - ✅ **Wildcard (*) expansion in URLs** (parsing directory listings)
18
+ - ✅ **Input from txt, csv, tsv files**
19
+ - ✅ **Progress bars** (total file count and individual download progress)
20
+ - ✅ **Ignore cache** (`--no-cache`, ignore `.part` files)
21
+ - ✅ **Basic auth / Bearer token support**
22
+ - ✅ **English/Korean comments and error messages**
23
+
24
+ - ✅ **wget/curl 스타일 옵션 지원** (`-O`, `-c`, `--limit-rate`)
25
+ - ✅ **멀티스레드 다운로드** (기본 4개 스레드)
26
+ - ✅ **속도 제한** (e.g., `--max-speed 1M`)
27
+ - ✅ **이어받기 지원** (`--resume` 또는 `-c`)
28
+ - ✅ **URL 내 와일드카드(*) 확장 지원** (디렉토리 리스트 파싱)
29
+ - ✅ **txt, csv, tsv 파일 입력 지원**
30
+ - ✅ **진행률 표시** (총 파일 수 및 개별 파일 다운로드)
31
+ - ✅ **캐시 무시 기능** (`--no-cache`, `.part` 파일 무시)
32
+ - ✅ **기본 인증 / Bearer 토큰 지원**
33
+ - ✅ **영어/한글 주석 및 에러 메시지**
34
+
35
+ ---
36
+
37
+ ## Installation / 설치
38
+
39
+ ```bash
40
+ pip install httpx tqdm
41
+ ```
42
+
43
+ Or go to release page and download the latest version. When you using build file, you don't need to install `httpx` and `tqdm`. Also don't add command `python` before `easyget.py`. Just type `easyget` and options.
44
+
45
+ 또는 릴리즈 페이지에서 최신 버전을 다운로드하세요. 빌드 파일을 사용할 때는 `httpx`와 `tqdm`를 설치할 필요가 없습니다. 또한 `easyget.py` 앞에 `python` 명령을 추가하지 마세요. 그냥 `easyget`과 옵션을 입력하세요.
46
+
47
+ ---
48
+
49
+ ## Usage / 사용법
50
+
51
+ ### 1. Single file download / 단일 파일 다운로드
52
+
53
+ ```bash
54
+ python easyget.py "https://example.com/file.zip"
55
+ ```
56
+
57
+ ### 2. Specify output filename / 파일명 지정
58
+
59
+ ```bash
60
+ python easyget.py "https://example.com/file.zip" -O myfile.zip
61
+ ```
62
+
63
+ ### 3. Resume download / 이어받기
64
+
65
+ ```bash
66
+ python easyget.py "https://example.com/file.zip" -c
67
+ ```
68
+
69
+ ### 4. Multi-threaded with speed limit / 멀티스레드 + 속도 제한
70
+
71
+ ```bash
72
+ python easyget.py "https://example.com/large.iso" --multi 8 --max-speed 2M
73
+ ```
74
+
75
+ ### 5. Use input file list (txt, csv, tsv) / URL 리스트로 다운로드
76
+
77
+ ```bash
78
+ python easyget.py urls.csv
79
+ ```
80
+
81
+ ### 6. Wildcard in URL / 와일드카드 URL 사용
82
+
83
+ ```bash
84
+ python easyget.py "https://example.com/files/*.zip"
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Input File Format / 입력 파일 형식
90
+
91
+ ### txt
92
+
93
+ ```
94
+ https://example.com/file1.zip
95
+ https://example.com/file2.zip
96
+ ```
97
+
98
+ ### csv or tsv
99
+
100
+ | url | filename |
101
+ |----------------------------|------------------|
102
+ | https://example.com/a.pdf | lecture_a.pdf |
103
+ | https://example.com/b.pdf | lecture_b.pdf |
104
+
105
+ ---
106
+
107
+ ## Advanced Options / 고급 옵션
108
+
109
+ | Option | Description / 설명 |
110
+ |--------------------------|---------------------|
111
+ | `--output`, `-O` | Output file name (단일 파일 다운로드 시) |
112
+ | `--resume`, `-c` | Resume download (이어받기) |
113
+ | `--multi` | Number of threads (스레드 수) |
114
+ | `--max-speed` | Download speed limit (e.g., 1M, 500K) |
115
+ | `--no-cache` | Ignore .part cache and force redownload |
116
+ | `--header` | Custom HTTP headers (e.g., `"Key: Value"`) |
117
+ | `--user-agent` | User-Agent 설정 |
118
+ | `--username`, `--password` | Basic 인증용 계정 정보 |
119
+ | `--token` | Bearer 토큰 인증 |
120
+
121
+ ---
122
+
123
+ ## Notes / 주의사항
124
+
125
+ - When a download is interrupted, a `.part` file is created.
126
+ - Use `--no-cache` to ignore existing `.part` files and redownload.
127
+ - Wildcard URLs are based on `href="..."` format in HTML directory listings.
128
+
129
+ - 다운로드가 중단되면 `.part` 파일이 생성됩니다.
130
+ - `--no-cache` 옵션을 사용하면 기존 `.part` 파일을 무시하고 새로 다운로드합니다.
131
+ - 와일드카드 URL은 HTML 디렉토리 리스트에서 `href="..."` 형식을 기반으로 파일을 찾습니다.
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ This project is licensed under the [MIT License](./LICENSE).
138
+
139
+ 이 프로젝트는 [MIT 라이선스](./LICENSE)를 따릅니다.
140
+
141
+ ---
142
+
143
+ ## Contribute / 기여
144
+ For questions or improvements, feel free to open an issue or pull request!
145
+
146
+ 기여하고 싶거나 궁금한 점이 있으면 언제든 이슈나 PR을 열어주세요!
File without changes
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ easyget: wget/curl compatible file downloader
4
+ easyget: wget/curl 호환 파일 다운로드 도구
5
+
6
+ This script downloads files using HTTP with features similar to wget and curl.
7
+ 이 스크립트는 wget과 curl과 유사한 기능을 제공하며 HTTP를 통해 파일을 다운로드합니다.
8
+
9
+ Features / 특징:
10
+ - Supports wget/curl style options (-O, -c, --limit-rate)
11
+ wget/curl 스타일 옵션(-O, -c, --limit-rate)을 지원합니다.
12
+ - Supports wildcard (*) in URLs to expand and download multiple files from a directory listing.
13
+ URL에 포함된 에스터리스크(*)를 확장하여 디렉토리 내 여러 파일을 다운로드할 수 있습니다.
14
+ - Accepts input as a txt, csv, or tsv file containing URLs and optional filenames.
15
+ txt, csv, tsv 파일로부터 URL 및 파일명을 읽어들입니다.
16
+ - Displays two progress bars: one for total file count and one for the current file's progress.
17
+ 전체 파일 개수 진행바(상단)와 현재 파일 다운로드 진행바(하단)를 표시합니다.
18
+ - Uses httpx.Client for efficient HTTP connection reuse.
19
+ 효율적인 HTTP 연결 재사용을 위해 httpx.Client를 사용합니다.
20
+ - Provides detailed bilingual (English and Korean) comments for beginners.
21
+ 초보자도 쉽게 이해할 수 있도록 영어와 한국어로 상세한 주석을 제공합니다.
22
+ - All error messages are output in English prefixed with "easyget error:".
23
+ 모든 오류 메시지는 "easyget error:" 접두어와 함께 영어로 출력됩니다.
24
+ - After download completes, the final filename and file size are displayed.
25
+ 다운로드 완료 후 최종 파일명과 파일 용량(바이트 단위)을 출력합니다.
26
+ - Supports a new option to ignore cache (--no-cache) which forces a fresh download.
27
+ 새 옵션(--no-cache)을 통해 캐시(이전의 .part 파일)를 무시하고 새로 다운로드할 수 있습니다.
28
+ """
29
+
30
+ import argparse
31
+ import os
32
+ import sys
33
+ import threading
34
+ import time
35
+ import logging
36
+ import base64
37
+ import csv
38
+ import fnmatch
39
+ import re
40
+ from urllib.parse import urlparse, urljoin
41
+ from typing import Optional, Dict, List, Tuple
42
+
43
+ import httpx
44
+ from tqdm import tqdm
45
+
46
+ # =============================
47
+ # Configuration Constants / 설정 상수
48
+ # =============================
49
+ CHUNK_SIZE = 1024 * 64 # 64KB - Size of each chunk to read / 청크 당 읽을 바이트 수
50
+ DEFAULT_THREADS = 4 # Default number of threads / 기본 스레드 수
51
+
52
+ # Global flags for file overwrite behavior / 파일 덮어쓰기 전역 변수
53
+ OVERWRITE_ALL = False # If True, all existing files will be overwritten / 모든 파일 덮어쓰기 허용
54
+ SKIP_ALL = False # If True, all existing files will be skipped / 모든 파일 건너뛰기
55
+
56
+ # Configure logging settings / 로깅 설정
57
+ logging.basicConfig(
58
+ # format="[%(asctime)s] %(levelname)s: %(message)s",
59
+ format="%(message)s", # Simplified format / 간소화된 형식
60
+ level=logging.INFO, # INFO level logging / 정보 수준 로그
61
+ # level=logging.DEBUG, # DEBUG level logging (디버깅 수준 로그)
62
+ # level=logging.ERROR, # ERROR level logging (오류 수준 로그)
63
+ datefmt="%H:%M:%S"
64
+ )
65
+
66
+
67
+ def get_filename_from_url(url: str) -> str:
68
+ """
69
+ Extract the filename from a URL.
70
+ URL에서 파일명을 추출합니다.
71
+
72
+ Parameters / 매개변수:
73
+ - url (str): The URL to extract the filename from. / 파일명을 추출할 URL
74
+
75
+ Returns / 반환값:
76
+ - str: The extracted filename or a default name if not found. / 추출된 파일명 또는 찾지 못할 경우 기본 이름
77
+ """
78
+ path = urlparse(url).path # Parse the URL path / URL 경로 파싱
79
+ return os.path.basename(path) or "downloaded.file" # Return basename or default / 기본 파일명 반환
80
+
81
+
82
+ def parse_speed(speed_str: str) -> Optional[int]:
83
+ """
84
+ Convert a speed limit string to bytes per second.
85
+ 속도 제한 문자열을 초당 바이트 단위로 변환합니다.
86
+
87
+ Supported examples: '1M', '500K', '0.5M', etc.
88
+ 지원 예시: '1M', '500K', '0.5M' 등.
89
+
90
+ If the value is less than 1, a warning is logged and None is returned.
91
+ 값이 1 미만인 경우 경고를 로그에 남기고 None을 반환합니다.
92
+
93
+ Parameters / 매개변수:
94
+ - speed_str (str): The speed limit string. / 속도 제한 문자열
95
+
96
+ Returns / 반환값:
97
+ - Optional[int]: Speed in bytes per second, or None if invalid. / 초당 바이트 단위 속도 또는 유효하지 않을 경우 None
98
+ """
99
+ try:
100
+ speed_str = speed_str.strip().upper() # Remove whitespace and convert to uppercase / 공백 제거 및 대문자 변환
101
+ if speed_str.endswith("M"):
102
+ speed = float(speed_str[:-1]) * 1024 * 1024
103
+ elif speed_str.endswith("K"):
104
+ speed = float(speed_str[:-1]) * 1024
105
+ else:
106
+ speed = float(speed_str)
107
+ speed_int = int(speed)
108
+ if speed_int < 1:
109
+ logging.warning(f"Invalid speed limit '{speed_str}' provided. It must be at least 1 byte per second. No speed limit applied.")
110
+ return None
111
+ return speed_int
112
+ except ValueError as e:
113
+ logging.warning(f"Error parsing speed limit '{speed_str}': {e}. No speed limit applied.")
114
+ return None
115
+
116
+
117
+ def get_file_size(url: str, headers: Dict[str, str], client: Optional[httpx.Client] = None) -> Optional[int]:
118
+ """
119
+ Retrieve the file size by sending an HTTP HEAD request.
120
+ HTTP HEAD 요청을 보내 파일 크기를 가져옵니다.
121
+
122
+ Parameters / 매개변수:
123
+ - url (str): The URL of the file. / 파일의 URL
124
+ - headers (Dict[str, str]): HTTP headers to include in the request. / 요청에 포함할 HTTP 헤더
125
+ - client (Optional[httpx.Client]): Optional reusable HTTP client. / 선택 사항: 재사용 가능한 HTTP 클라이언트
126
+
127
+ Returns / 반환값:
128
+ - Optional[int]: The file size in bytes, or None if unavailable. / 파일 크기(바이트) 또는 사용 불가능할 경우 None
129
+ """
130
+ try:
131
+ if client:
132
+ response = client.head(url, headers=headers, follow_redirects=True, timeout=10)
133
+ else:
134
+ response = httpx.head(url, headers=headers, follow_redirects=True, timeout=10)
135
+ if 'content-length' in response.headers:
136
+ return int(response.headers['content-length'])
137
+ except Exception as e:
138
+ logging.error(f"easyget error: Failed to get file size: {e}")
139
+ return None
140
+
141
+
142
+ def alias_wget_style(args: argparse.Namespace) -> argparse.Namespace:
143
+ """
144
+ Map wget style options (e.g., -O) to our arguments.
145
+ wget 스타일 옵션(-O 등)을 명령행 인수에 매핑합니다.
146
+
147
+ Parameters / 매개변수:
148
+ - args (argparse.Namespace): Parsed command-line arguments. / 파싱된 명령행 인수
149
+
150
+ Returns / 반환값:
151
+ - argparse.Namespace: Updated arguments with wget style mappings. / wget 스타일 매핑이 적용된 인수
152
+ """
153
+ if args.output is None and '-O' in sys.argv:
154
+ idx = sys.argv.index('-O')
155
+ if idx + 1 < len(sys.argv):
156
+ args.output = sys.argv[idx + 1]
157
+ return args
158
+
159
+
160
+ def alias_wget_curl_style(args: argparse.Namespace) -> argparse.Namespace:
161
+ """
162
+ Map additional wget/curl style options.
163
+ 추가 wget/curl 스타일 옵션을 매핑합니다.
164
+
165
+ Options mapped:
166
+ - '-c' for resume (like wget) / '-c' 옵션은 이어받기(resume) 기능
167
+ - '--limit-rate' for download speed limit / '--limit-rate' 옵션은 다운로드 속도 제한
168
+
169
+ Parameters / 매개변수:
170
+ - args (argparse.Namespace): Parsed command-line arguments. / 파싱된 명령행 인수
171
+
172
+ Returns / 반환값:
173
+ - argparse.Namespace: Updated arguments with additional mappings. / 추가 매핑이 적용된 인수
174
+ """
175
+ if not args.resume and '-c' in sys.argv:
176
+ args.resume = True
177
+
178
+ if args.max_speed is None:
179
+ for arg in sys.argv:
180
+ if arg.startswith("--limit-rate"):
181
+ if "=" in arg:
182
+ _, value = arg.split("=", 1)
183
+ args.max_speed = value
184
+ else:
185
+ idx = sys.argv.index(arg)
186
+ if idx + 1 < len(sys.argv):
187
+ args.max_speed = sys.argv[idx + 1]
188
+ return args
189
+
190
+
191
+ class SpeedLimiter:
192
+ """
193
+ A class to limit download speed.
194
+ 다운로드 속도를 제한하기 위한 클래스입니다.
195
+
196
+ Attributes / 속성:
197
+ - max_speed (int): Maximum speed in bytes per second. / 초당 최대 속도 (바이트)
198
+ - start_time (Optional[float]): Timestamp when download started. / 다운로드 시작 시간
199
+ - downloaded (int): Total bytes downloaded so far. / 지금까지 다운로드한 총 바이트 수
200
+ """
201
+ def __init__(self, max_speed: int):
202
+ self.max_speed = max_speed # Set maximum speed / 최대 속도 설정
203
+ self.start_time: Optional[float] = None
204
+ self.downloaded = 0
205
+
206
+ def wait(self, chunk_size: int) -> None:
207
+ """
208
+ Pause the download to maintain the speed limit.
209
+ 속도 제한을 유지하기 위해 다운로드를 일시 정지합니다.
210
+
211
+ Parameters / 매개변수:
212
+ - chunk_size (int): The size of the downloaded chunk in bytes. / 다운로드된 청크의 바이트 크기
213
+ """
214
+ if self.start_time is None:
215
+ self.start_time = time.time() # Set start time at first call / 첫 호출 시 시작 시간 설정
216
+ self.downloaded += chunk_size
217
+ elapsed = time.time() - self.start_time
218
+ expected = self.downloaded / self.max_speed
219
+ if expected > elapsed:
220
+ time.sleep(expected - elapsed) # Sleep for the remaining time / 남은 시간 동안 대기
221
+
222
+
223
+ def download_range(url: str, start: int, end: int, headers: Dict[str, str],
224
+ output_path: str, pbar: tqdm, limiter: Optional[SpeedLimiter] = None,
225
+ client: Optional[httpx.Client] = None,
226
+ error_event: Optional[threading.Event] = None) -> None:
227
+ """
228
+ Download a specific byte range of a file in a separate thread.
229
+ 별도의 스레드에서 파일의 특정 바이트 범위를 다운로드합니다.
230
+
231
+ Parameters / 매개변수:
232
+ - url (str): The URL of the file. / 파일의 URL
233
+ - start (int): Starting byte of the range. / 시작 바이트
234
+ - end (int): Ending byte of the range. / 종료 바이트
235
+ - headers (Dict[str, str]): HTTP headers to use. / 사용할 HTTP 헤더
236
+ - output_path (str): Path to the temporary output file. / 임시 출력 파일 경로
237
+ - pbar (tqdm): Progress bar to update download progress. / 다운로드 진행 상황을 업데이트할 진행바
238
+ - limiter (Optional[SpeedLimiter]): Optional speed limiter instance. / 선택 사항: 속도 제한 인스턴스
239
+ - client (Optional[httpx.Client]): Optional reusable HTTP client. / 선택 사항: 재사용 가능한 HTTP 클라이언트
240
+ - error_event (Optional[threading.Event]): Shared event to signal an error. / 오류 발생 시 공유 이벤트
241
+ """
242
+ range_header = headers.copy()
243
+ range_header['Range'] = f'bytes={start}-{end}' # Set the HTTP Range header / HTTP Range 헤더 설정
244
+ try:
245
+ if client:
246
+ stream = client.stream("GET", url, headers=range_header, follow_redirects=True, timeout=30)
247
+ else:
248
+ stream = httpx.stream("GET", url, headers=range_header, follow_redirects=True, timeout=30)
249
+ with stream as response:
250
+ # Check HTTP status code before proceeding / 다운로드 시작 전 HTTP 상태 코드 확인
251
+ if response.status_code >= 400:
252
+ logging.error(f"easyget error: HTTP error {response.status_code} when downloading range {start}-{end} from {url}")
253
+ if error_event:
254
+ error_event.set()
255
+ return
256
+ with open(output_path, 'r+b') as f:
257
+ f.seek(start) # Move file pointer to the start position / 파일 포인터를 시작 위치로 이동
258
+ for chunk in response.iter_bytes(CHUNK_SIZE):
259
+ if limiter:
260
+ limiter.wait(len(chunk)) # Apply speed limit if specified / 속도 제한 적용
261
+ f.write(chunk) # Write the chunk to the file / 청크를 파일에 기록
262
+ pbar.update(len(chunk)) # Update progress bar / 진행바 업데이트
263
+ except Exception as e:
264
+ logging.error(f"easyget error: Error downloading range {start}-{end}: {e}")
265
+ if error_event:
266
+ error_event.set()
267
+
268
+
269
+ def safe_rename(tmp_path: str, output: str) -> bool:
270
+ """
271
+ Safely rename the temporary file to the final output filename.
272
+ 임시 파일을 최종 파일명으로 안전하게 변경합니다.
273
+
274
+ If the output file already exists, prompt the user for action:
275
+ - y: Overwrite this file.
276
+ - n: Skip this file.
277
+ - a: Overwrite all files.
278
+ - i: Skip all files.
279
+
280
+ 파일이 이미 존재하는 경우 사용자에게 다음 옵션을 묻습니다:
281
+ - y: 현재 파일 덮어쓰기
282
+ - n: 현재 파일 건너뛰기
283
+ - a: 모든 파일 덮어쓰기 허용
284
+ - i: 모든 파일 건너뛰기
285
+
286
+ Returns / 반환값:
287
+ - bool: True if the file was successfully renamed (or overwritten), False if skipped.
288
+ / 파일 변경(덮어쓰기) 성공 시 True, 건너뛰면 False.
289
+ """
290
+ global OVERWRITE_ALL, SKIP_ALL
291
+ if os.path.exists(output):
292
+ if SKIP_ALL:
293
+ logging.error(f"easyget error: File {output} already exists. Skipping file.")
294
+ os.remove(tmp_path)
295
+ return False
296
+ if not OVERWRITE_ALL:
297
+ prompt = f"File '{output}' already exists. Overwrite? (y = yes, n = no, a = all yes, i = all no): "
298
+ while True:
299
+ answer = input(prompt).strip().lower()
300
+ if answer == 'y':
301
+ break
302
+ elif answer == 'n':
303
+ logging.error(f"easyget error: File {output} already exists. Skipping file.")
304
+ os.remove(tmp_path)
305
+ return False
306
+ elif answer == 'a':
307
+ OVERWRITE_ALL = True
308
+ break
309
+ elif answer == 'i':
310
+ SKIP_ALL = True
311
+ logging.error(f"easyget error: File {output} already exists. Skipping file.")
312
+ os.remove(tmp_path)
313
+ return False
314
+ else:
315
+ print("Please enter y (yes), n (no), a (all yes), or i (all no).")
316
+ try:
317
+ os.remove(output) # Remove existing file / 기존 파일 삭제
318
+ except Exception as e:
319
+ logging.error(f"easyget error: Failed to remove existing file {output}: {e}")
320
+ os.remove(tmp_path)
321
+ return False
322
+ try:
323
+ os.rename(tmp_path, output) # Rename temporary file to final output / 임시 파일을 최종 파일명으로 변경
324
+ return True
325
+ except Exception as e:
326
+ logging.error(f"easyget error: Failed to rename file from {tmp_path} to {output}: {e}")
327
+ return False
328
+
329
+
330
+ def download_file(url: str, output: str, resume: bool = False, threads: int = DEFAULT_THREADS,
331
+ max_speed: Optional[str] = None, headers: Optional[Dict[str, str]] = None,
332
+ progress_position: Optional[int] = None, client: Optional[httpx.Client] = None,
333
+ ignore_cache: bool = False) -> None:
334
+ """
335
+ Download a file from the given URL, supporting multi-threading, resume functionality,
336
+ and an option to ignore cache.
337
+ 주어진 URL로부터 파일을 다운로드합니다. 멀티스레드, 이어받기 기능 및 캐시 무시 옵션을 지원합니다.
338
+
339
+ Parameters / 매개변수:
340
+ - url (str): The URL to download. / 다운로드할 URL
341
+ - output (str): The output filename. / 저장할 파일명
342
+ - resume (bool): Whether to resume an interrupted download. / 중단된 다운로드 이어받기 여부
343
+ - threads (int): Number of threads for multi-threaded download. / 멀티스레드 다운로드에 사용할 스레드 수
344
+ - max_speed (Optional[str]): Maximum download speed (e.g., "1M", "500K"). / 최대 다운로드 속도
345
+ - headers (Optional[Dict[str, str]]): HTTP headers to use. / 사용할 HTTP 헤더
346
+ - progress_position (Optional[int]): Position for the progress bar (tqdm). / 진행바 표시 위치
347
+ - client (Optional[httpx.Client]): Optional reusable HTTP client. / 선택 사항: 재사용 가능한 HTTP 클라이언트
348
+ - ignore_cache (bool): If True, ignore any existing cache (.part file) and force a fresh download.
349
+ If enabled, any existing temporary file is removed before starting download.
350
+ / True인 경우 기존 캐시(.part 파일)를 무시하고 새로 다운로드합니다.
351
+ 활성화되면 기존 임시 파일을 삭제하고 새로 다운로드합니다.
352
+ """
353
+ headers = headers or {}
354
+ tmp_path = output + ".part" # Temporary file path / 임시 파일 경로
355
+
356
+ # If ignore_cache is enabled, remove any existing temporary file
357
+ # 캐시 무시 옵션이 활성화된 경우, 기존의 임시 파일(.part 파일)을 삭제합니다.
358
+ if ignore_cache and os.path.exists(tmp_path):
359
+ try:
360
+ os.remove(tmp_path)
361
+ logging.info(f"Ignoring cache: Removed existing temporary file {tmp_path}")
362
+ except Exception as e:
363
+ logging.error(f"easyget error: Failed to remove cache file {tmp_path}: {e}")
364
+
365
+ total_size = get_file_size(url, headers, client=client) # Get the total file size / 전체 파일 크기 확인
366
+
367
+ # If file size is unknown, force single-threaded download
368
+ if total_size is None:
369
+ logging.info("File size unknown. Downloading using a single thread.")
370
+ threads = 1
371
+
372
+ # In multi-threaded resume, if resume is enabled, switch to single-thread mode
373
+ if resume and threads > 1:
374
+ logging.warning("easyget error: Resume feature may not work properly in multi-threaded mode. Switching to single thread.")
375
+ threads = 1
376
+
377
+ downloaded_size = 0
378
+ mode = 'wb'
379
+ if resume and os.path.exists(tmp_path):
380
+ downloaded_size = os.path.getsize(tmp_path)
381
+ if total_size and downloaded_size < total_size:
382
+ headers['Range'] = f'bytes={downloaded_size}-'
383
+ mode = 'ab'
384
+ elif total_size and downloaded_size >= total_size:
385
+ logging.info("The file appears to be fully downloaded already.")
386
+ if safe_rename(tmp_path, output):
387
+ try:
388
+ final_size = os.path.getsize(output)
389
+ logging.info(f"Download complete: {output} ({final_size} bytes)")
390
+ except Exception as e:
391
+ logging.error(f"easyget error: Failed to get final file size for {output}: {e}")
392
+ return
393
+
394
+ limiter: Optional[SpeedLimiter] = None
395
+ if max_speed:
396
+ parsed_speed = parse_speed(max_speed)
397
+ if parsed_speed:
398
+ limiter = SpeedLimiter(parsed_speed)
399
+
400
+ # Single-threaded download branch / 단일 스레드 다운로드 분기
401
+ if threads == 1:
402
+ try:
403
+ if client:
404
+ response = client.stream("GET", url, headers=headers, follow_redirects=True, timeout=60)
405
+ else:
406
+ response = httpx.stream("GET", url, headers=headers, follow_redirects=True, timeout=60)
407
+ with response as resp:
408
+ # Check HTTP status code before downloading content / 다운로드 시작 전 HTTP 상태 코드 확인
409
+ if resp.status_code >= 400:
410
+ logging.error(f"easyget error: HTTP error {resp.status_code} when downloading {url}")
411
+ return
412
+ with open(tmp_path, mode) as f, tqdm(
413
+ total=total_size, initial=downloaded_size, unit='B', unit_scale=True,
414
+ desc=output, position=progress_position, leave=False
415
+ ) as pbar:
416
+ for chunk in resp.iter_bytes(CHUNK_SIZE):
417
+ f.write(chunk)
418
+ pbar.update(len(chunk))
419
+ except Exception as e:
420
+ logging.error(f"easyget error: Download failed: {e}")
421
+ return
422
+ else:
423
+ # Multi-threaded download branch / 멀티스레드 다운로드 분기
424
+ if not os.path.exists(tmp_path):
425
+ try:
426
+ with open(tmp_path, 'wb') as f:
427
+ if total_size:
428
+ f.truncate(total_size) # Pre-allocate file space / 파일 공간 미리 할당
429
+ except Exception as e:
430
+ logging.error(f"easyget error: Failed to create temporary file: {e}")
431
+ return
432
+
433
+ error_event = threading.Event() # Shared event to signal error among threads / 스레드 간 오류 발생을 알리기 위한 공유 이벤트
434
+ ranges: List[Tuple[int, int]] = []
435
+ part_size = total_size // threads if total_size else 0
436
+ for i in range(threads):
437
+ start = i * part_size
438
+ # Ensure the last thread gets any remaining bytes / 마지막 스레드가 남은 바이트를 받도록 설정
439
+ end = total_size - 1 if total_size and i == threads - 1 else (start + part_size - 1)
440
+ ranges.append((start, end))
441
+
442
+ with tqdm(total=total_size, unit='B', unit_scale=True,
443
+ desc=output, position=progress_position, leave=False) as pbar:
444
+ threads_list = []
445
+ for start, end in ranges:
446
+ t = threading.Thread(
447
+ target=download_range,
448
+ args=(url, start, end, headers, tmp_path, pbar, limiter, client, error_event)
449
+ )
450
+ threads_list.append(t)
451
+ t.start()
452
+ for t in threads_list:
453
+ t.join()
454
+ # If any thread encountered an error, abort the download / 스레드 중 하나라도 오류가 발생하면 다운로드를 중단합니다.
455
+ if error_event.is_set():
456
+ logging.error(f"easyget error: Download aborted due to HTTP errors while downloading {url}")
457
+ try:
458
+ os.remove(tmp_path)
459
+ except Exception:
460
+ pass
461
+ return
462
+
463
+ if safe_rename(tmp_path, output):
464
+ try:
465
+ final_size = os.path.getsize(output)
466
+ logging.info(f"Download complete: {output} ({final_size} bytes)")
467
+ except Exception as e:
468
+ logging.error(f"easyget error: Failed to get final file size for {output}: {e}")
469
+
470
+
471
+ def parse_file_list(file_path: str) -> List[Tuple[str, str]]:
472
+ """
473
+ Parse an input file (txt, csv, or tsv) to extract a list of (URL, filename) tuples.
474
+ txt, csv, tsv 파일을 파싱하여 (URL, filename) 튜플 리스트를 반환합니다.
475
+
476
+ For txt files, each line is treated as a URL.
477
+ txt 파일의 경우 각 줄을 URL로 처리합니다.
478
+
479
+ For csv/tsv files, use the "url" column and the "filename" column (if available).
480
+ csv/tsv 파일의 경우 "url" 컬럼과 "filename" 컬럼(존재할 경우)을 사용합니다.
481
+
482
+ Parameters / 매개변수:
483
+ - file_path (str): Path to the input file. / 입력 파일 경로
484
+
485
+ Returns / 반환값:
486
+ - List[Tuple[str, str]]: A list of tuples, each containing a URL and its corresponding filename.
487
+ / 각 튜플이 URL과 해당 파일명을 포함하는 리스트
488
+ """
489
+ file_list: List[Tuple[str, str]] = []
490
+ ext = os.path.splitext(file_path)[1].lower() # Get the file extension / 파일 확장자 추출
491
+ try:
492
+ with open(file_path, encoding='utf-8') as f:
493
+ if ext == '.txt':
494
+ for line in f:
495
+ line = line.strip()
496
+ if line:
497
+ file_list.append((line, get_filename_from_url(line)))
498
+ elif ext in ['.csv', '.tsv']:
499
+ delimiter = ',' if ext == '.csv' else '\t' # Set delimiter based on file type / 파일 형식에 따른 구분자 설정
500
+ reader = csv.DictReader(f, delimiter=delimiter)
501
+ for row in reader:
502
+ url_val = row.get("url")
503
+ if not url_val:
504
+ continue
505
+ filename_val = row.get("filename") or get_filename_from_url(url_val)
506
+ file_list.append((url_val.strip(), filename_val.strip()))
507
+ else:
508
+ logging.error("easyget error: Unsupported file format. Supported formats: txt, csv, tsv.")
509
+ except Exception as e:
510
+ logging.error(f"easyget error: Failed to parse file list: {e}")
511
+ return file_list
512
+
513
+
514
+ def expand_wildcard_url(url: str, headers: Dict[str, str], client: httpx.Client) -> List[Tuple[str, str]]:
515
+ """
516
+ If the URL contains an asterisk (*), expand it by retrieving the directory listing and matching the pattern.
517
+ URL에 에스터리스크(*)가 포함된 경우, 디렉토리 목록을 가져와 패턴과 일치하는 파일 링크를 확장합니다.
518
+
519
+ For example, given: http://example.com/files/*.zip
520
+ 예를 들어: http://example.com/files/*.zip
521
+
522
+ Parameters / 매개변수:
523
+ - url (str): The URL containing a wildcard. / 와일드카드가 포함된 URL
524
+ - headers (Dict[str, str]): HTTP headers to use. / 사용할 HTTP 헤더
525
+ - client (httpx.Client): Reusable HTTP client. / 재사용 가능한 HTTP 클라이언트
526
+
527
+ Returns / 반환값:
528
+ - List[Tuple[str, str]]: List of (full URL, filename) tuples for each matching file.
529
+ / 매칭된 각 파일에 대한 (전체 URL, 파일명) 튜플 리스트
530
+ """
531
+ parsed = urlparse(url)
532
+ base_path = os.path.dirname(parsed.path) # Extract directory path / 디렉토리 경로 추출
533
+ pattern = os.path.basename(parsed.path) # Extract the wildcard pattern / 와일드카드 패턴 추출
534
+ base_url = f"{parsed.scheme}://{parsed.netloc}{base_path}/" # Build base URL / 기본 URL 생성
535
+ try:
536
+ response = client.get(base_url, headers=headers, timeout=30)
537
+ if response.status_code != 200:
538
+ logging.error(f"easyget error: Failed to retrieve directory listing from {base_url} (Status code: {response.status_code})")
539
+ return []
540
+ links = re.findall(r'href="([^"]+)"', response.text)
541
+ matched_links = [link for link in links if fnmatch.fnmatch(link, pattern)]
542
+ file_list = []
543
+ for link in matched_links:
544
+ full_url = urljoin(base_url, link)
545
+ filename = os.path.basename(link) or get_filename_from_url(full_url)
546
+ file_list.append((full_url, filename))
547
+ if not file_list:
548
+ logging.error(f"easyget error: No files matching pattern '{pattern}' were found.")
549
+ return file_list
550
+ except Exception as e:
551
+ logging.error(f"easyget error: Error during wildcard expansion: {e}")
552
+ return []
553
+
554
+
555
+ def main() -> None:
556
+ """
557
+ Main function that parses command-line arguments and initiates the download process.
558
+ 명령행 인수를 파싱하고 다운로드 프로세스를 시작하는 메인 함수입니다.
559
+ """
560
+ parser = argparse.ArgumentParser(description="easyget: wget/curl compatible file downloader")
561
+ parser.add_argument("input", help="URL to download or a file path (txt, csv, tsv) containing URLs (supports wildcard *)")
562
+ parser.add_argument("-o", "--output", help="Output filename (for single URL download)")
563
+ parser.add_argument("--resume", action="store_true", help="Resume interrupted download (equivalent to -c)")
564
+ parser.add_argument("--multi", type=int, default=DEFAULT_THREADS, help="Number of threads for multi-threaded download")
565
+ parser.add_argument("--max-speed", help="Maximum download speed (e.g., 1M, 500K)")
566
+ parser.add_argument("--limit-rate", help="Wget style maximum download speed (e.g., 1M, 500K)")
567
+ parser.add_argument("--user-agent", help="Specify the User-Agent header")
568
+ parser.add_argument("--username", help="Username for basic authentication")
569
+ parser.add_argument("--password", help="Password for basic authentication")
570
+ parser.add_argument("--token", help="Bearer token for authentication")
571
+ parser.add_argument("--header", action="append", help="Additional HTTP header (format: key:value)")
572
+ # New option: ignore cache / 새 옵션: 캐시 무시 (이전 다운로드된 .part 파일 무시)
573
+ parser.add_argument("--no-cache", action="store_true", help="Ignore cached partial downloads and force a fresh download (ignore .part files)")
574
+
575
+ args = parser.parse_args()
576
+ args = alias_wget_style(args)
577
+ args = alias_wget_curl_style(args)
578
+
579
+ headers: Dict[str, str] = {}
580
+ if args.username and args.password:
581
+ userpass = f"{args.username}:{args.password}"
582
+ headers['Authorization'] = 'Basic ' + base64.b64encode(userpass.encode()).decode()
583
+ elif args.token:
584
+ headers['Authorization'] = f"Bearer {args.token}"
585
+
586
+ if args.user_agent:
587
+ headers['User-Agent'] = args.user_agent
588
+
589
+ if args.header:
590
+ for h in args.header:
591
+ if ':' in h:
592
+ key, value = h.split(':', 1)
593
+ headers[key.strip()] = value.strip()
594
+
595
+ # Use httpx.Client for efficient HTTP requests / 효율적인 HTTP 요청을 위해 httpx.Client 사용
596
+ with httpx.Client() as client:
597
+ # If the input is a file (txt, csv, tsv), parse it for URLs / 입력이 파일인 경우 (txt, csv, tsv) URL 파싱
598
+ if os.path.exists(args.input) and args.input.lower().endswith(('.txt', '.csv', '.tsv')):
599
+ file_list = parse_file_list(args.input)
600
+ if not file_list:
601
+ logging.error("easyget error: No files to download. Please check your input file.")
602
+ sys.exit(1)
603
+ global_pbar = tqdm(total=len(file_list), desc="Total Files", position=0)
604
+ for url, output in file_list:
605
+ download_file(url, output, resume=args.resume, threads=args.multi,
606
+ max_speed=args.max_speed, headers=headers, progress_position=1, client=client,
607
+ ignore_cache=args.no_cache)
608
+ global_pbar.update(1)
609
+ global_pbar.close()
610
+ # If the input URL contains a wildcard (*) expand it / URL에 와일드카드(*)가 포함된 경우 확장
611
+ elif '*' in args.input:
612
+ file_list = expand_wildcard_url(args.input, headers, client)
613
+ if not file_list:
614
+ logging.error("easyget error: No files found for wildcard expansion.")
615
+ sys.exit(1)
616
+ global_pbar = tqdm(total=len(file_list), desc="Total Files", position=0)
617
+ for url, output in file_list:
618
+ download_file(url, output, resume=args.resume, threads=args.multi,
619
+ max_speed=args.max_speed, headers=headers, progress_position=1, client=client,
620
+ ignore_cache=args.no_cache)
621
+ global_pbar.update(1)
622
+ global_pbar.close()
623
+ else:
624
+ # Single URL download / 단일 URL 다운로드
625
+ url = args.input
626
+ output = args.output or get_filename_from_url(url)
627
+ download_file(url, output, resume=args.resume, threads=args.multi,
628
+ max_speed=args.max_speed, headers=headers, progress_position=0, client=client,
629
+ ignore_cache=args.no_cache)
630
+
631
+
632
+ if __name__ == "__main__":
633
+ try:
634
+ main()
635
+ except KeyboardInterrupt:
636
+ logging.info("Download interrupted by user.")
637
+ sys.exit(1)
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.2
2
+ Name: easyget
3
+ Version: 1.0.0
4
+ Summary: Fast, easy-to-use multi-platform file downloader
5
+ Author: Your Name
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Operating System :: OS Independent
8
+ Requires-Python: >=3.7
9
+ Requires-Dist: httpx>=0.27.0
10
+ Requires-Dist: tqdm>=4.60.0
11
+ Dynamic: author
12
+ Dynamic: classifier
13
+ Dynamic: requires-dist
14
+ Dynamic: requires-python
15
+ Dynamic: summary
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ easyget/__init__.py
5
+ easyget/__main__.py
6
+ easyget.egg-info/PKG-INFO
7
+ easyget.egg-info/SOURCES.txt
8
+ easyget.egg-info/dependency_links.txt
9
+ easyget.egg-info/entry_points.txt
10
+ easyget.egg-info/requires.txt
11
+ easyget.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ easyget = easyget.__main__:main
@@ -0,0 +1,2 @@
1
+ httpx>=0.27.0
2
+ tqdm>=4.60.0
@@ -0,0 +1 @@
1
+ easyget
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
easyget-1.0.0/setup.py ADDED
@@ -0,0 +1,23 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="easyget",
5
+ version="1.0.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ "httpx>=0.27.0",
9
+ "tqdm>=4.60.0"
10
+ ],
11
+ entry_points={
12
+ "console_scripts": [
13
+ "easyget=easyget.__main__:main",
14
+ ]
15
+ },
16
+ author="Your Name",
17
+ description="Fast, easy-to-use multi-platform file downloader",
18
+ classifiers=[
19
+ "Programming Language :: Python :: 3",
20
+ "Operating System :: OS Independent",
21
+ ],
22
+ python_requires=">=3.7",
23
+ )