Skip to content

Commit 6d1d8a1

Browse files
committed
Merge branch 'release/v1.1.0'
2 parents e6eb051 + a4397e6 commit 6d1d8a1

28 files changed

+650
-152
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.torrent filter=lfs diff=lfs merge=lfs -text

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
__pycache__
12
docs/build/
2-
src/__pycache__/
33
Pipfile.lock
44
dist
55
src/py3createtorrent.egg-info
66
build
77
testdata/
88
.idea/
9-
*.torrent
9+
benchmark/*.csv
10+
benchmark/*.png
11+
benchmark/py3createtorrent.py
12+
benchmark/results
File renamed without changes.

BUILD.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ python setup.py bdist_wheel
55

66
# Uploading
77

8-
twine upload dist/*
9-
twine upload --repository testpypi dist/*
8+
twine upload --skip-existing dist/*
9+
twine upload --skip-existing --repository testpypi dist/*
1010

1111
# Testing
1212

Pipfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ sphinx = "~=3.1"
88
mypy = "~=0.782"
99
yapf = "~=0.30"
1010
twine = "*"
11+
matplotlib = "*"
12+
pandas = "*"
13+
torf-cli = {file = "https://github.com/rndusr/torf-cli/archive/refs/heads/master.zip"}
14+
faker = "*"
1115

1216
[packages]
1317
"bencode.py" = "~=4.0"

README.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@ Features
1010

1111
Some of the features:
1212

13-
* you can create **huge torrents** for any amount of data
14-
* you can add a **comment** to the torrent file
15-
* you can create **private torrents** (disabled DHT, ...)
16-
* you can create torrents with **multiple trackers**
17-
* you can create **trackerless torrents**
18-
* you can add **webseeds** to torrents
19-
* you can **exclude specific files/folders**
20-
* you can exclude files/folders based on **regular expressions**
21-
* you can specify **custom piece sizes**
22-
* you can specify custom creation dates
13+
* create torrents with **multiple trackers** or **trackerless torrents**
14+
* **automatically choose the most reliable trackers** from [ngosang/trackerslist](https://github.com/ngosang/trackerslist)
15+
* fast torrent creation thanks to **multithreading**
16+
* add **webseeds** to torrents
17+
* create **private torrents** (disabled DHT, ...)
18+
* **exclude specific files/folders**
19+
* exclude files/folders based on **regular expressions**
20+
* specify **custom piece sizes**
21+
* specify custom creation dates
2322

2423
Basic usage
2524
-----------

benchmark/Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM debian:stable
2+
3+
WORKDIR /benchmark
4+
5+
RUN apt-get update
6+
RUN apt-get install -y mktorrent transmission-cli python3 python3-pip wget
7+
8+
RUN wget https://github.com/sharkdp/hyperfine/releases/download/v1.13.0/hyperfine_1.13.0_amd64.deb
9+
RUN dpkg -i hyperfine_1.13.0_amd64.deb
10+
11+
RUN pip3 install py3createtorrent torf-cli matplotlib pandas
12+
13+
COPY py3createtorrent.py benchmark.sh create_random_file.py create_random_folder.py plot_benchmark_results.py ./

benchmark/benchmark.bat

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
set size=15g
2+
set target=../tests/testdata/random_file_%size%.dat
3+
set warmup=1
4+
set runs=3
5+
set threads=1,4
6+
7+
python create_random_file.py ../tests/testdata/random_file_%size%.dat %size%
8+
9+
hyperfine --warmup %warmup% --runs %runs% --export-csv benchmark_results.csv -L threads %threads% -L piece_size 128,1024,8192 "python ../src/py3createtorrent.py %target% -p {piece_size} --threads {threads}" "torrenttools create %target% -v1 --piece-size {piece_size}K --threads {threads}"
10+
11+
hyperfine --warmup %warmup% --runs %runs% --export-csv benchmark_results_torf.csv -L threads %threads% -L piece_size 0.125,1,8 --show-output "torf %target% --yes --threads {threads} --max-piece-size {piece_size}"
12+
13+
python plot_benchmark_results.py benchmark_results.csv benchmark_results_torf.csv

benchmark/benchmark.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/sh
2+
target=random_file_4gib.dat
3+
python3 create_random_file.py $target 1g
4+
5+
warmup=1
6+
runs=3
7+
threads=1,2,3,4
8+
9+
hyperfine --warmup $warmup --runs $runs --export-csv /results/benchmark_results.csv -L threads $threads -L piece_size 128,1024,8192 --show-output "python3 py3createtorrent.py $target -p {piece_size} --threads {threads}" "transmission-create --piece-size {piece_size} $target"
10+
11+
hyperfine --warmup $warmup --runs $runs --export-csv /results/benchmark_results_torf.csv -L threads $threads -L piece_size 0.125,1,8 --show-output "torf $target --yes --threads {threads} --max-piece-size {piece_size}"
12+
13+
hyperfine --warmup $warmup --runs $runs --prepare "rm *.torrent" --export-csv /results/benchmark_results_mktorrent.csv -L threads $threads -L piece_size 17,20,23 --show-output "mktorrent -t{threads} -l{piece_size} $target"
14+
15+
cd /results
16+
python3 /benchmark/plot_benchmark_results.py benchmark_results.csv benchmark_results_torf.csv benchmark_results_mktorrent.csv

benchmark/benchmark_docker.bat

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
copy /Y ..\src\py3createtorrent.py .
2+
docker build . --tag benchmark
3+
docker container rm benchmark
4+
docker container run --name benchmark -v %cd%\results:/results benchmark sh /benchmark/benchmark.sh

benchmark/create_random_file.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
Script to create files of given size with pseudo-random content.
3+
"""
4+
import argparse
5+
import os
6+
import random
7+
import sys
8+
9+
10+
def parse_size(size):
11+
letter_to_unit = {"K": 2**10, "M": 2**20, "G": 2**30}
12+
13+
error_message = ("must be a number ending with a single letter to "
14+
"indicate the unit type K, M or G - for example, use 256k to specify 256 KiB")
15+
16+
letter = size[-1]
17+
if letter.isdigit():
18+
raise argparse.ArgumentTypeError(error_message)
19+
20+
number = size[:-1]
21+
if not number.isdigit():
22+
raise argparse.ArgumentTypeError(error_message)
23+
24+
return letter_to_unit[letter.upper()] * int(number)
25+
26+
27+
def create_random_file(dst, size):
28+
BLOCK_SIZE = 4096
29+
bytes_saved = 0
30+
with open(dst, "wb") as fh:
31+
while True:
32+
remaining_bytes = size - bytes_saved
33+
if remaining_bytes:
34+
data = random.randbytes(min(remaining_bytes, BLOCK_SIZE))
35+
bytes_saved += len(data)
36+
fh.write(data)
37+
else:
38+
break
39+
40+
41+
def main():
42+
parser = argparse.ArgumentParser()
43+
parser.add_argument("path", help="destination file path")
44+
parser.add_argument(
45+
"size",
46+
type=parse_size,
47+
help="size in KiB/MiB/GiB, specify unit with a single suffix letter K/M/G, for example 256k for 256 KiB")
48+
parser.add_argument("--seed", type=int, default=0, help="Set seed for the random number generator.")
49+
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing file.")
50+
51+
args = parser.parse_args()
52+
53+
if os.path.isfile(args.path) and not args.overwrite:
54+
if os.path.getsize(args.path) != args.size:
55+
print("ERROR: Destination file already exists BUT DOES NOT HAVE THE CORRECT SIZE", file=sys.stderr)
56+
else:
57+
print("WARNING: Destination file already exists (already has the requested size)", file=sys.stderr)
58+
print("Not doing anything. Use --overwrite option to force overwriting the existing file.")
59+
sys.exit(1)
60+
61+
random.seed(args.seed)
62+
create_random_file(args.path, args.size)
63+
64+
65+
if __name__ == '__main__':
66+
main()

benchmark/create_random_folder.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Script to generate a specified number of files with pseudo-random names/contents in
3+
a given directory. The size of each file will be randomly (uniformly) chosen from a
4+
user-specified range.
5+
"""
6+
import argparse
7+
import random
8+
from pathlib import Path
9+
10+
from create_random_file import parse_size, create_random_file
11+
from faker import Faker
12+
13+
14+
def create_cache_dir_tag(path):
15+
path = Path(path)
16+
if path.is_dir():
17+
path = path.joinpath("CACHEDIR.TAG")
18+
elif not path.name == "CACHEDIR.TAG":
19+
raise ValueError("Full file path was specified, but file name is not CACHEDIR.TAG")
20+
21+
with open(path, "w") as fh:
22+
fh.write("Signature: 8a477f597d28d172789f06886806bc55\n")
23+
fh.write("# This file instructs backup applications to ignore this directory.\n")
24+
fh.write("# For more information see https://www.brynosaurus.com/cachedir/\n")
25+
26+
return path
27+
28+
29+
def main():
30+
parser = argparse.ArgumentParser()
31+
parser.add_argument("path", type=Path, help="destination folder path")
32+
parser.add_argument("number_of_files", type=int, help="Number of files to generate")
33+
parser.add_argument(
34+
"min_file_size",
35+
type=parse_size,
36+
help=
37+
"minimum file size in KiB/MiB/GiB, specify unit with a single suffix letter K/M/G, for example 256k for 256 KiB"
38+
)
39+
parser.add_argument("max_file_size", type=parse_size, help="maximum file size")
40+
parser.add_argument("--seed", type=int, default=0, help="Set seed for the random number generator.")
41+
parser.add_argument("--no-cachedir-tag", action="store_true", help="Do not generate CACHEDIR.TAG file")
42+
43+
args = parser.parse_args()
44+
45+
if args.min_file_size > args.max_file_size:
46+
parser.error("min_file_size must be smaller or equal than max_file_size")
47+
48+
random.seed(args.seed)
49+
fake = Faker()
50+
Faker.seed(args.seed)
51+
52+
args.path.mkdir(parents=True, exist_ok=True)
53+
if not args.no_cachedir_tag:
54+
p = create_cache_dir_tag(args.path)
55+
print("Saved cachedir tag at: %s" % p)
56+
57+
for i in range(args.number_of_files):
58+
filename = fake.file_name()
59+
size = random.randint(args.min_file_size, args.max_file_size)
60+
print("Creating random file: % 20s of size: % 10d bytes..." % (filename, size))
61+
create_random_file(args.path.joinpath(filename), size)
62+
63+
64+
if __name__ == '__main__':
65+
main()

benchmark/plot_benchmark_results.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import argparse
2+
import os
3+
4+
import matplotlib.pyplot as plt
5+
import pandas as pd
6+
7+
8+
def generate_plot_for_piece_size(df, piece_size):
9+
df = df[df["parameter_piece_size"] == piece_size].copy()
10+
df = df.drop(columns="parameter_piece_size")
11+
12+
TOOLS = ["py3createtorrent", "torrenttools", "torf", "mktorrent", "transmission-create"]
13+
14+
for tool in TOOLS:
15+
df.loc[df["command"].str.contains(tool), "tool"] = tool
16+
17+
df = df.drop(columns="command")
18+
df = df.set_index(["parameter_threads", "tool"])
19+
20+
tools = df.index.unique(level=1)
21+
threads = df.index.unique(level=0)
22+
max_threads = max(threads)
23+
24+
fig, ax = plt.subplots(1, 1, figsize=(8, 6))
25+
26+
for idx, tool in enumerate(tools):
27+
mean = df.loc[df.index.get_level_values(1) == tool, "mean"]
28+
stddev = df.loc[df.index.get_level_values(1) == tool, "stddev"]
29+
ax.plot(threads, mean)
30+
#ax.errorbar(threads, mean, stddev, linestyle='None', marker='x')
31+
32+
fig.suptitle("Performance (lower = faster)", fontsize=20)
33+
ax.set_title("Piece size = %d KiB" % piece_size)
34+
ax.set_xlabel("Number of threads for hashing")
35+
ax.set_ylabel("Time in s")
36+
ax.set_xticks(list(range(1, max_threads + 1)))
37+
ax.legend(tools, loc='upper right')
38+
39+
fig.savefig("plot_for_piece_size_%dk.png" % piece_size, dpi=125)
40+
plt.close(fig)
41+
42+
43+
def main():
44+
parser = argparse.ArgumentParser()
45+
parser.add_argument("results_file", help="path to CSV file with the results", nargs='+')
46+
47+
args = parser.parse_args()
48+
49+
for file in args.results_file:
50+
if not os.path.isfile(file):
51+
parser.error("The specified results file does not exist: " + file)
52+
53+
df = None
54+
for file in args.results_file:
55+
df_file = pd.read_csv(file)
56+
if df is None:
57+
df = df_file
58+
else:
59+
df = pd.concat([df, df_file])
60+
61+
# Normalize piece sizes
62+
#print(df[df["command"].str.contains("torf")].head())
63+
df.loc[df["command"].str.contains("torf"), "parameter_piece_size"] *= 2**10
64+
df.loc[df["command"].str.contains("mktorrent"), "parameter_piece_size"] = 2**(df.loc[df["command"].str.contains("mktorrent"), "parameter_piece_size"] - 10)
65+
66+
#print(df)
67+
piece_sizes = df["parameter_piece_size"].unique()
68+
69+
plt.style.use('ggplot')
70+
for p in piece_sizes:
71+
print("Generating plot for piece size %s" % p)
72+
generate_plot_for_piece_size(df.copy(), p)
73+
print()
74+
75+
76+
if __name__ == '__main__':
77+
main()

docs/source/changelog.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Changelog
22
=========
33

4+
Version 1.1.0
5+
-------------
6+
7+
*Release date: 2022/03/18*
8+
9+
* added: **multithreading** for much faster torrent creation (about 30-40% faster). The number of threads can be controlled with the new ``--threads`` option. It defaults to using 4 threads which will be a good choice on most systems.
10+
* added: ``--version`` command
11+
* improved: formatting and content of ``--help`` output
12+
413
Version 1.0.1
514
-------------
615

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@
4848
# built documents.
4949
#
5050
# The short X.Y version.
51-
version = '1.0'
51+
version = '1.1'
5252
# The full version, including alpha/beta/rc tags.
53-
release = '1.0.1'
53+
release = '1.1.0'
5454

5555
# The language for content autogenerated by Sphinx. Refer to documentation
5656
# for a list of supported languages.

0 commit comments

Comments
 (0)