feature: add global.torrent settings card

feature: remember make torrent options
pull/3671/head
chalkim 2024-06-21 02:28:47 +08:00
parent 9ff78ded0e
commit 1d1cc4b7f8
24 changed files with 476 additions and 231 deletions

View File

@ -17,4 +17,4 @@ EXPOSE 80
COPY docker_config.json /.filebrowser.json COPY docker_config.json /.filebrowser.json
COPY filebrowser /filebrowser COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ] ENTRYPOINT [ "/filebrowser" ]

View File

@ -14,4 +14,4 @@ COPY filebrowser /usr/bin/filebrowser
# ports and volumes # ports and volumes
VOLUME /srv /config /database VOLUME /srv /config /database
EXPOSE 80 EXPOSE 80

View File

@ -337,6 +337,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Download: true, Download: true,
Torrent: true, Torrent: true,
}, },
CreateBody: users.CreateTorrentBody{
Date: true,
},
}, },
AuthMethod: "", AuthMethod: "",
Branding: settings.Branding{}, Branding: settings.Branding{},

View File

@ -78,7 +78,7 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.String("locale", "en", "locale for users") flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users") flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only") flags.Bool("singleClick", false, "use single clicks only")
flags.String("trackerListsUrl", "https://cf.trackerslist.com/all.txt", "tracker lists url") flags.String("trackersListUrl", "https://cf.trackerslist.com/all.txt", "tracker lists url")
} }
func getViewMode(flags *pflag.FlagSet) users.ViewMode { func getViewMode(flags *pflag.FlagSet) users.ViewMode {

View File

@ -48,6 +48,7 @@ options you want to change.`,
Perm: user.Perm, Perm: user.Perm,
Sorting: user.Sorting, Sorting: user.Sorting,
Commands: user.Commands, Commands: user.Commands,
CreateBody: user.CreateBody,
} }
getUserDefaults(flags, &defaults, false) getUserDefaults(flags, &defaults, false)
user.Scope = defaults.Scope user.Scope = defaults.Scope
@ -57,6 +58,7 @@ options you want to change.`,
user.Perm = defaults.Perm user.Perm = defaults.Perm
user.Commands = defaults.Commands user.Commands = defaults.Commands
user.Sorting = defaults.Sorting user.Sorting = defaults.Sorting
user.CreateBody = defaults.CreateBody
user.LockPassword = mustGetBool(flags, "lockPassword") user.LockPassword = mustGetBool(flags, "lockPassword")
if newUsername != "" { if newUsername != "" {

View File

@ -1,5 +1,10 @@
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils"; import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
export async function fetchDefaultOptions() {
const url = `/api/torrent`
return fetchJSON(url);
}
export async function makeTorrent( export async function makeTorrent(
url: string, url: string,
announces: string[], announces: string[],

View File

@ -2,7 +2,7 @@
<div class="card floating"> <div class="card floating">
<div class="card-content"> <div class="card-content">
<p> <p>
{{ $t("prompts.publishMessageSingle") }} {{ $t("prompts.publishMessage") }}
</p> </p>
</div> </div>
<div class="card-action"> <div class="card-action">
@ -65,9 +65,9 @@ export default {
this.req.items[this.selected[0]].url this.req.items[this.selected[0]].url
).then(() => { ).then(() => {
buttons.success("publish"); buttons.success("publish");
this.$showSuccess(this.$t("prompts.publishSuccess")); this.$showSuccess(this.$t("success.torrentPublished"));
}).catch((e) => { }).catch((e) => {
buttons.done("delete"); buttons.done("publish");
this.$showError(e); this.$showError(e);
}); });
}; };

View File

@ -2,44 +2,85 @@
<div class="card floating" style="max-width: 50em;" id="share"> <div class="card floating" style="max-width: 50em;" id="share">
<div class="card-title"> <div class="card-title">
<h2>{{ $t("buttons.torrent") }}</h2> <h2>{{ $t("buttons.torrent") }}</h2>
<button class="button button--flat button--blue" @click="toggleDetailedView">
{{ detailedView ? $t("buttons.compact") : $t("buttons.detailed") }}
</button>
</div> </div>
<div class="card-content"> <div class="card-content">
<!--
<p>{{ $t("prompts.name") }}</p>
<input
class="input input--block"
type="text"
v-model.trim="name"
tabindex="0" />
-->
<p v-if="detailedView">{{ $t("prompts.pieceLength") }}</p>
<select
v-if="detailedView"
class="input input--block"
v-model.trim="pieceLen"
tabindex="1">
<option value="0">{{ $t("prompts.auto") }}</option>
<option value="15">32 KiB</option>
<option value="16">64 KiB</option>
<option value="17">128 KiB</option>
<option value="18">256 KiB</option>
<option value="19">512 KiB</option>
<option value="20">1 MiB</option>
<option value="21">2 MiB</option>
<option value="22">4 MiB</option>
<option value="23">8 MiB</option>
<option value="24">16 MiB</option>
<option value="25">32 MiB</option>
<option value="26">64 MiB</option>
<option value="27">128 MiB</option>
<option value="28">256 MiB</option>
</select>
<p>{{ $t("prompts.trackersList") }}</p> <p>{{ $t("prompts.trackersList") }}</p>
<textarea <textarea
class="input input--block input--textarea" class="input input--block input--textarea"
style="min-height: 5em;" style="min-height: 4em;"
type="text" type="text"
v-model.trim="announces" v-model.trim="announces"
tabindex="1"></textarea> tabindex="2"></textarea>
<p>{{ $t("prompts.webSeeds") }}</p> <p>{{ $t("prompts.webSeeds") }}</p>
<textarea <textarea
class="input input--block input--textarea" class="input input--block input--textarea"
style="min-height: 5em;" style="min-height: 4em;"
type="text" type="text"
v-model.trim="webSeeds" v-model.trim="webSeeds"
tabindex="2"></textarea> tabindex="3"></textarea>
<p>{{ $t("prompts.comment") }}</p> <p>{{ $t("prompts.comment") }}</p>
<textarea <textarea
class="input input--block input--textarea" class="input input--block input--textarea"
style="min-height: 5em;" style="min-height: 4em;"
type="text" type="text"
v-model.trim="comment" v-model.trim="comment"
tabindex="3"></textarea> tabindex="4"></textarea>
<p>{{ $t("prompts.source") }}</p> <p v-if="detailedView">{{ $t("prompts.source") }}</p>
<input <input
v-if="detailedView"
class="input input--block" class="input input--block"
type="text" type="text"
v-model.trim="source" v-model.trim="source"
tabindex="4" /> tabindex="5" />
<label> <p v-if="detailedView">
<input type="checkbox" v-model="privateFlag" tabindex="3" /> <input type="checkbox" v-model="date" tabindex="6" />
{{ $t("prompts.includeDate") }}
</p>
<p v-if="detailedView">
<input type="checkbox" v-model="privateFlag" tabindex="7" />
{{ $t("prompts.privateTorrent") }} {{ $t("prompts.privateTorrent") }}
</label> </p>
</div> </div>
<div class="card-action"> <div class="card-action">
@ -70,10 +111,11 @@ export default {
comment: "", comment: "",
date: true, date: true,
name: "", name: "",
pieceLen: 18, pieceLen: 0,
privateFlag: false, privateFlag: false,
source: "", source: "",
webSeeds: [], webSeeds: [],
detailedView: false
}; };
}, },
inject: ["$showError", "$showSuccess"], inject: ["$showError", "$showSuccess"],
@ -99,25 +141,21 @@ export default {
}, },
}, },
async beforeMount() { async beforeMount() {
this.fetchTrackers(); api.fetchDefaultOptions().then((res) => {
this.announces = res.announces.join("\n");
this.comment = res.comment;
this.date = res.date;
this.name = res.name;
this.pieceLen = res.pieceLen;
this.privateFlag = res.private;
this.source = res.source;
this.webSeeds = res.webSeeds.join("\n");
});
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers"]), ...mapActions(useLayoutStore, ["closeHovers"]),
async fetchTrackers() { toggleDetailedView() {
const url = 'https://cf.trackerslist.com/all.txt'; this.detailedView = !this.detailedView;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(this.$t("error.failedToFetchTrackers"));
}
let text = await response.text();
text = text.replace(/^\s*[\r\n]/gm, '');
this.announces = text;
} catch (error) {
this.$showError(error);
text = this.$t("error.faildToFetchTrackers");
this.announces = text;
}
}, },
torrent: async function (event) { torrent: async function (event) {
event.preventDefault(); event.preventDefault();
@ -135,10 +173,10 @@ export default {
this.comment, this.comment,
this.date, this.date,
this.name, this.name,
this.pieceLen, parseInt(this.pieceLen),
this.privateFlag, this.privateFlag,
this.source, this.source,
this.webSeeds, this.webSeeds.split("\n").map((t) => t.trim()).filter((t) => t),
).then( ).then(
() => { () => {
buttons.success("torrent"); buttons.success("torrent");
@ -147,9 +185,9 @@ export default {
this.$showSuccess(this.$t("success.torrentCreated")); this.$showSuccess(this.$t("success.torrentCreated"));
} }
).catch((e) => { ).catch((e) => {
buttons.done("torrent"); buttons.done("torrent");
this.$showError(e); this.$showError(e);
}); });
} }
this.closeHovers(); this.closeHovers();

View File

@ -42,7 +42,9 @@
"openFile": "打开文件", "openFile": "打开文件",
"continue": "继续", "continue": "继续",
"fullScreen": "切换全屏", "fullScreen": "切换全屏",
"discardChanges": "放弃更改" "discardChanges": "放弃更改",
"detailed": "转到详细视图",
"compact": "转到精简视图"
}, },
"download": { "download": {
"downloadFile": "下载文件", "downloadFile": "下载文件",
@ -144,11 +146,15 @@
"resolution": "分辨率", "resolution": "分辨率",
"deleteUser": "你确定要删除这个用户吗?", "deleteUser": "你确定要删除这个用户吗?",
"discardEditorChanges": "你确定要放弃所做的更改吗?", "discardEditorChanges": "你确定要放弃所做的更改吗?",
"pieceLength": "分片大小:",
"auto": "自动",
"trackersList": "Tracker URL", "trackersList": "Tracker URL",
"comment": "注释:", "comment": "注释:",
"webSeeds": "Web 种子 URL", "webSeeds": "Web 种子 URL",
"source": "源:", "source": "源:",
"privateTorrent": "私有torrent不会在DHT网络上分发" "includeDate": "写入创建日期",
"privateTorrent": "私有 torrent 不会在DHT网络上分发",
"publishMessage": "你确定要发布这个种子吗?"
}, },
"search": { "search": {
"images": "图像", "images": "图像",
@ -245,7 +251,16 @@
"userManagement": "用户管理", "userManagement": "用户管理",
"userUpdated": "用户已更新!", "userUpdated": "用户已更新!",
"username": "用户名", "username": "用户名",
"users": "用户" "users": "用户",
"torrentSettings": "BT设置",
"makeTorrent": "制种",
"makeTorrentSettingsDescription": "你可以在此设置BT种子*.torrent的默认参数。这些参数可以在创建种子时修改。",
"trackersListUrl": "Tracker URL 列表来源",
"publish": "发布",
"qbSettingsDescription": "你可以在此设置 qBittorrent WebUI 的地址、用户名和密码。",
"qbUrl": "qBittorrent WebUI 地址",
"qbUsername": "qBittorrent WebUI 用户名",
"qbPassword": "qBittorrent WebUI 密码"
}, },
"sidebar": { "sidebar": {
"help": "帮助", "help": "帮助",
@ -262,7 +277,8 @@
}, },
"success": { "success": {
"linkCopied": "链接已复制!", "linkCopied": "链接已复制!",
"torrentCreated": "种子已创建!" "torrentCreated": "种子已创建!",
"torrentPublished": "种子已发布!"
}, },
"time": { "time": {
"days": "天", "days": "天",

View File

@ -8,6 +8,7 @@ interface ISettings {
tus: SettingsTus; tus: SettingsTus;
shell: string[]; shell: string[];
commands: SettingsCommand; commands: SettingsCommand;
torrent: SettingsTorrent;
} }
interface SettingsDefaults { interface SettingsDefaults {
@ -49,6 +50,13 @@ interface SettingsCommand {
before_upload?: string[]; before_upload?: string[];
} }
interface SettingsTorrent {
trackersListUrl?: string;
qbUrl?: string;
qbUsername?: string;
qbPassword?: string;
}
interface SettingsUnit { interface SettingsUnit {
KB: number; KB: number;
MB: number; MB: number;

View File

@ -225,6 +225,67 @@
</div> </div>
</form> </form>
</div> </div>
<div class="column">
<form class="card" @submit.prevent="save">
<div class="card-title">
<h2>{{ t("settings.torrentSettings") }}</h2>
</div>
<div class="card-content">
<h3>{{ t("settings.makeTorrent") }}</h3>
<p class="small">{{ t("settings.makeTorrentSettingsDescription") }}</p>
<p>
<label for="trackersListUrl">{{ t("settings.trackersListUrl") }}</label>
<input
class="input input--block"
type="text"
v-model="settings.torrent.trackersListUrl"
id="trackersListUrl"
/>
</p>
<h3>{{ t("settings.publish") }}</h3>
<p class="small">{{ t("settings.qbSettingsDescription") }}</p>
<p>
<label for="qbUrl">{{ t("settings.qbUrl") }}</label>
<input
class="input input--block"
v-model.trim="settings.torrent.qbUrl"
id="qbUrl"
></input>
</p>
<p>
<label for="qbUsername">{{ t("settings.qbUsername") }}</label>
<input
class="input input--block"
v-model.trim="settings.torrent.qbUsername"
id="qbUsername"
></input>
</p>
<p>
<label for="qbPassword">{{ t("settings.qbPassword") }}</label>
<input
class="input input--block"
type="password"
placeholder="●●●●●●●●"
v-model.trim="settings.torrent.qbPassword"
id="qbPassword"
></input>
</p>
</div>
<div class="card-action">
<input
class="button button--flat"
type="submit"
:value="t('buttons.update')"
/>
</div>
</form>
</div>
</div> </div>
</template> </template>

View File

@ -11,6 +11,7 @@ import (
"github.com/filebrowser/filebrowser/v2/runner" "github.com/filebrowser/filebrowser/v2/runner"
"github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage" "github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/torrent"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
) )
@ -18,6 +19,7 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, d *data) (int, erro
type data struct { type data struct {
*runner.Runner *runner.Runner
*torrent.Torrent
settings *settings.Settings settings *settings.Settings
server *settings.Server server *settings.Server
store *storage.Storage store *storage.Storage
@ -61,6 +63,7 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin
status, err := fn(w, r, &data{ status, err := fn(w, r, &data{
Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings}, Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings},
Torrent: &torrent.Torrent{Settings: settings},
store: store, store: store,
settings: settings, settings: settings,
server: server, server: server,

View File

@ -78,6 +78,7 @@ func NewHandler(
api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST") api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST")
api.PathPrefix("/share").Handler(monkey(shareDeleteHandler, "/api/share")).Methods("DELETE") api.PathPrefix("/share").Handler(monkey(shareDeleteHandler, "/api/share")).Methods("DELETE")
api.PathPrefix("/torrent").Handler(monkey(torrentGetHandler, "/api/torrent")).Methods("Get")
api.PathPrefix("/torrent").Handler(monkey(torrentPostHandler, "/api/torrent")).Methods("POST") api.PathPrefix("/torrent").Handler(monkey(torrentPostHandler, "/api/torrent")).Methods("POST")
api.PathPrefix("/publish").Handler(monkey(publishPostHandler, "/api/publish")).Methods("POST") api.PathPrefix("/publish").Handler(monkey(publishPostHandler, "/api/publish")).Methods("POST")

View File

@ -18,10 +18,11 @@ type settingsData struct {
Tus settings.Tus `json:"tus"` Tus settings.Tus `json:"tus"`
Shell []string `json:"shell"` Shell []string `json:"shell"`
Commands map[string][]string `json:"commands"` Commands map[string][]string `json:"commands"`
Torrent settings.Torrent `json:"torrent"`
} }
var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
data := &settingsData{ data := settingsData{
Signup: d.settings.Signup, Signup: d.settings.Signup,
CreateUserDir: d.settings.CreateUserDir, CreateUserDir: d.settings.CreateUserDir,
UserHomeBasePath: d.settings.UserHomeBasePath, UserHomeBasePath: d.settings.UserHomeBasePath,
@ -31,8 +32,11 @@ var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
Tus: d.settings.Tus, Tus: d.settings.Tus,
Shell: d.settings.Shell, Shell: d.settings.Shell,
Commands: d.settings.Commands, Commands: d.settings.Commands,
Torrent: d.settings.Torrent,
} }
data.Torrent.QbPassword = ""
return renderJSON(w, r, data) return renderJSON(w, r, data)
}) })
@ -52,6 +56,7 @@ var settingsPutHandler = withAdmin(func(_ http.ResponseWriter, r *http.Request,
d.settings.Tus = req.Tus d.settings.Tus = req.Tus
d.settings.Shell = req.Shell d.settings.Shell = req.Shell
d.settings.Commands = req.Commands d.settings.Commands = req.Commands
d.settings.Torrent = req.Torrent
err = d.store.Settings.Save(d.settings) err = d.store.Settings.Save(d.settings)
return errToStatus(err), err return errToStatus(err), err

View File

@ -4,11 +4,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os/exec"
"path/filepath" "path/filepath"
"github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/torrent" "github.com/filebrowser/filebrowser/v2/users"
) )
func withPermTorrent(fn handleFunc) handleFunc { func withPermTorrent(fn handleFunc) handleFunc {
@ -21,97 +20,15 @@ func withPermTorrent(fn handleFunc) handleFunc {
}) })
} }
/* var torrentGetHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
mktorrent 1.1 (c) 2007, 2009 Emil Renner Berthing // return default torrent options
s, err := d.GetDefaultCreateBody(d.user.CreateBody)
Usage: mktorrent [OPTIONS] <target directory or filename> if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to get default create body: %w", err)
Options:
-a, --announce=<url>[,<url>]* : specify the full announce URLs
at least one is required
additional -a adds backup trackers
-c, --comment=<comment> : add a comment to the metainfo
-d, --no-date : don't write the creation date
-h, --help : show this help screen
-l, --piece-length=<n> : set the piece length to 2^n bytes,
default is 18, that is 2^18 = 256kb
-n, --name=<name> : set the name of the torrent
default is the basename of the target
-o, --output=<filename> : set the path and filename of the created file
default is <name>.torrent
-p, --private : set the private flag
-s, --source=<source> : add source string embedded in infohash
-t, --threads=<n> : use <n> threads for calculating hashes
default is the number of CPU cores
-v, --verbose : be verbose
-w, --web-seed=<url>[,<url>]* : add web seed URLs
additional -w adds more URLs
*/
// TorrentOptions 结构体定义了 mktorrent 命令的选项
type TorrentOptions struct {
Target string // 目标文件或目录路径
Name string // 种子名称
OutputFile string // 输出种子文件的路径和文件名
Announces []string // 主要的 tracker URLs
Comment string // 添加的注释
Date bool // 是否写入创建日期
PieceLen int // 设置块大小
Private bool // 是否设置为私有种子
Source string // 添加到 infohash 中的源字符串
Threads int // 使用的线程数
WebSeeds []string // Web seed URLs
}
// buildArgs 函数根据 TorrentOptions 构建 mktorrent 命令的参数列表
func buildArgs(opts TorrentOptions) []string {
args := []string{}
for _, announce := range opts.Announces {
args = append(args, "-a", announce)
} }
if opts.Comment != "" { return renderJSON(w, r, s)
args = append(args, "-c", opts.Comment) })
}
if !opts.Date {
args = append(args, "-d")
}
if opts.PieceLen > 0 {
args = append(args, "-l", fmt.Sprintf("%d", opts.PieceLen))
}
if opts.Name != "" {
args = append(args, "-n", opts.Name)
}
if opts.OutputFile != "" {
args = append(args, "-o", opts.OutputFile)
}
if opts.Private {
args = append(args, "-p")
}
if opts.Source != "" {
args = append(args, "-s", opts.Source)
}
if opts.Threads > 0 {
args = append(args, "-t", fmt.Sprintf("%d", opts.Threads))
}
if len(opts.WebSeeds) > 0 {
for _, ws := range opts.WebSeeds {
args = append(args, "-w", ws)
}
}
args = append(args, opts.Target)
return args
}
var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
file, err := files.NewFileInfo(&files.FileOptions{ file, err := files.NewFileInfo(&files.FileOptions{
@ -128,8 +45,7 @@ var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Req
} }
fPath := file.RealPath() fPath := file.RealPath()
var s *torrent.Torrent var body users.CreateTorrentBody
var body torrent.CreateBody
if r.Body != nil { if r.Body != nil {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return http.StatusBadRequest, fmt.Errorf("failed to decode body: %w", err) return http.StatusBadRequest, fmt.Errorf("failed to decode body: %w", err)
@ -137,36 +53,19 @@ var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Req
defer r.Body.Close() defer r.Body.Close()
} }
// 设置 mktorrent 命令的选项 err = d.Torrent.MakeTorrent(fPath, body)
options := TorrentOptions{
Target: fPath,
Announces: body.Announces,
Comment: body.Comment,
Date: body.Date,
Name: body.Name,
OutputFile: fPath + ".torrent",
PieceLen: body.PieceLen,
Private: body.Private,
Source: body.Source,
WebSeeds: body.WebSeeds,
}
// 构建 mktorrent 命令的参数列表
args := buildArgs(options)
cmd := exec.Command("mktorrent", args...)
err = cmd.Run()
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
s = &torrent.Torrent{ d.user.CreateBody = body
Path: fPath + ".torrent",
err = d.store.Users.Update(d.user)
if err != nil {
return http.StatusInternalServerError, err
} }
return renderJSON(w, r, s) return renderJSON(w, r, nil)
}) })
var publishPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var publishPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
@ -186,25 +85,13 @@ var publishPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Req
// only folder path // only folder path
fPath := filepath.Dir(tPath) fPath := filepath.Dir(tPath)
qbURL := "http://localhost:8081" // 修改为你的qBittorrent URL torrentPath := tPath
username := "moezakura" // 修改为你的用户名 savePath := fPath
password := "moezakura" // 修改为你的密码
torrentPath := tPath // 修改为你的本地torrent文件路径
savePath := fPath // 修改为你的保存路径
client := &http.Client{} err = d.Torrent.PublishTorrent(torrentPath, savePath)
sid, err := torrent.Login(client, qbURL, username, password)
if err != nil { if err != nil {
fmt.Printf("Error logging in: %v\n", err) return http.StatusInternalServerError, err
} }
err = torrent.AddTorrent(client, qbURL, sid, torrentPath, savePath)
if err != nil {
fmt.Printf("Error adding torrent: %v\n", err)
}
fmt.Println("Torrent added successfully!")
return renderJSON(w, r, nil) return renderJSON(w, r, nil)
}) })

View File

@ -8,16 +8,16 @@ import (
// UserDefaults is a type that holds the default values // UserDefaults is a type that holds the default values
// for some fields on User. // for some fields on User.
type UserDefaults struct { type UserDefaults struct {
Scope string `json:"scope"` Scope string `json:"scope"`
Locale string `json:"locale"` Locale string `json:"locale"`
ViewMode users.ViewMode `json:"viewMode"` ViewMode users.ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"` SingleClick bool `json:"singleClick"`
Sorting files.Sorting `json:"sorting"` Sorting files.Sorting `json:"sorting"`
Perm users.Permissions `json:"perm"` Perm users.Permissions `json:"perm"`
Commands []string `json:"commands"` Commands []string `json:"commands"`
HideDotfiles bool `json:"hideDotfiles"` HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"` DateFormat bool `json:"dateFormat"`
TrackerListsUrl string `json:"trackerListsUrl"` CreateBody users.CreateTorrentBody `json:"createBody"`
} }
// Apply applies the default options to a user. // Apply applies the default options to a user.
@ -31,5 +31,5 @@ func (d *UserDefaults) Apply(u *users.User) {
u.Commands = d.Commands u.Commands = d.Commands
u.HideDotfiles = d.HideDotfiles u.HideDotfiles = d.HideDotfiles
u.DateFormat = d.DateFormat u.DateFormat = d.DateFormat
u.TrackerListsUrl = d.TrackerListsUrl u.CreateBody = d.CreateBody
} }

View File

@ -27,6 +27,7 @@ type Settings struct {
Commands map[string][]string `json:"commands"` Commands map[string][]string `json:"commands"`
Shell []string `json:"shell"` Shell []string `json:"shell"`
Rules []rules.Rule `json:"rules"` Rules []rules.Rule `json:"rules"`
Torrent Torrent `json:"torrent"`
} }
// GetRules implements rules.Provider. // GetRules implements rules.Provider.

8
settings/torrent.go Normal file
View File

@ -0,0 +1,8 @@
package settings
type Torrent struct {
TrackersListUrl string `json:"trackersListUrl"`
QbUrl string `json:"qbUrl"`
QbUsername string `json:"qbUsername"`
QbPassword string `json:"qbPassword"`
}

View File

@ -135,6 +135,9 @@ func importConf(db *storm.DB, path string, sto *storage.Storage) error {
Share: true, Share: true,
Download: true, Download: true,
}, },
CreateBody: users.CreateTorrentBody{
Date: true,
},
}, },
} }

97
torrent/mktorrent.go Normal file
View File

@ -0,0 +1,97 @@
package torrent
import (
"fmt"
)
/*
mktorrent 1.1 (c) 2007, 2009 Emil Renner Berthing
Usage: mktorrent [OPTIONS] <target directory or filename>
Options:
-a, --announce=<url>[,<url>]* : specify the full announce URLs
at least one is required
additional -a adds backup trackers
-c, --comment=<comment> : add a comment to the metainfo
-d, --no-date : don't write the creation date
-h, --help : show this help screen
-l, --piece-length=<n> : set the piece length to 2^n bytes,
default is 18, that is 2^18 = 256kb
-n, --name=<name> : set the name of the torrent
default is the basename of the target
-o, --output=<filename> : set the path and filename of the created file
default is <name>.torrent
-p, --private : set the private flag
-s, --source=<source> : add source string embedded in infohash
-t, --threads=<n> : use <n> threads for calculating hashes
default is the number of CPU cores
-v, --verbose : be verbose
-w, --web-seed=<url>[,<url>]* : add web seed URLs
additional -w adds more URLs
*/
// TorrentOptions 结构体定义了 mktorrent 命令的选项
type TorrentOptions struct {
Target string // 目标文件或目录路径
Name string // 种子名称
OutputFile string // 输出种子文件的路径和文件名
Announces []string // 主要的 tracker URLs
Comment string // 添加的注释
Date bool // 是否写入创建日期
PieceLen int // 设置块大小
Private bool // 是否设置为私有种子
Source string // 添加到 infohash 中的源字符串
Threads int // 使用的线程数
WebSeeds []string // Web seed URLs
}
// buildArgs 函数根据 TorrentOptions 构建 mktorrent 命令的参数列表
func buildArgs(opts *TorrentOptions) []string {
args := []string{}
for _, announce := range opts.Announces {
args = append(args, "-a", announce)
}
if opts.Comment != "" {
args = append(args, "-c", opts.Comment)
}
if !opts.Date {
args = append(args, "-d")
}
if opts.PieceLen > 0 {
args = append(args, "-l", fmt.Sprintf("%d", opts.PieceLen))
}
if opts.Name != "" {
args = append(args, "-n", opts.Name)
}
if opts.OutputFile != "" {
args = append(args, "-o", opts.OutputFile)
}
if opts.Private {
args = append(args, "-p")
}
if opts.Source != "" {
args = append(args, "-s", opts.Source)
}
if opts.Threads > 0 {
args = append(args, "-t", fmt.Sprintf("%d", opts.Threads))
}
if len(opts.WebSeeds) > 0 {
for _, ws := range opts.WebSeeds {
args = append(args, "-w", ws)
}
}
args = append(args, opts.Target)
return args
}

View File

@ -10,9 +10,10 @@ import (
"path/filepath" "path/filepath"
) )
func Login(client *http.Client, qbURL, username, password string) (string, error) { func login(client *http.Client, qbURL, username, password string) (string, error) {
loginURL := fmt.Sprintf("%s/api/v2/auth/login", qbURL) loginURL := fmt.Sprintf("%s/api/v2/auth/login", qbURL)
data := fmt.Sprintf("username=%s&password=%s", username, password) data := fmt.Sprintf("username=%s&password=%s", username, password)
req, err := http.NewRequest("POST", loginURL, bytes.NewBufferString(data)) req, err := http.NewRequest("POST", loginURL, bytes.NewBufferString(data))
if err != nil { if err != nil {
return "", err return "", err
@ -39,7 +40,7 @@ func Login(client *http.Client, qbURL, username, password string) (string, error
return "", fmt.Errorf("SID cookie not found") return "", fmt.Errorf("SID cookie not found")
} }
func AddTorrent(client *http.Client, qbURL, sid, torrentPath, savePath string) error { func addTorrent(client *http.Client, qbURL, sid, torrentPath, savePath string) error {
addTorrentURL := fmt.Sprintf("%s/api/v2/torrents/add", qbURL) addTorrentURL := fmt.Sprintf("%s/api/v2/torrents/add", qbURL)
file, err := os.Open(torrentPath) file, err := os.Open(torrentPath)

View File

@ -1,30 +1,124 @@
package torrent package torrent
type CreateBody struct { import (
Announces []string `json:"announces"` "fmt"
Comment string `json:"comment"` "io"
Date bool `json:"date"` "net/http"
Name string `json:"name"` "os/exec"
PieceLen int `json:"pieceLen"` "strings"
Private bool `json:"private"`
Source string `json:"source"` "github.com/filebrowser/filebrowser/v2/settings"
WebSeeds []string `json:"webSeeds"` "github.com/filebrowser/filebrowser/v2/users"
} )
type Torrent struct { type Torrent struct {
Path string `json:"Path"` *settings.Settings
*users.User
} }
// Link is the information needed to build a shareable link. func (t *Torrent) MakeTorrent(fPath string, body users.CreateTorrentBody) error {
// type Torrent struct { tPath := fPath + ".torrent"
// Hash string `json:"hash" storm:"id,index"`
// Path string `json:"path" storm:"index"` // 设置 mktorrent 命令的选项
// UserID uint `json:"userID"` opts := &TorrentOptions{
// Expire int64 `json:"expire"` Target: fPath,
// PasswordHash string `json:"password_hash,omitempty"` Announces: body.Announces,
// // Token is a random value that will only be set when PasswordHash is set. It is Comment: body.Comment,
// // URL-Safe and is used to download links in password-protected shares via a Date: body.Date,
// // query arg. Name: body.Name,
// Token string `json:"token,omitempty"` OutputFile: tPath,
// } PieceLen: body.PieceLen,
// Private: body.Private,
Source: body.Source,
WebSeeds: body.WebSeeds,
}
args := buildArgs(opts)
cmd := exec.Command("mktorrent", args...)
err := cmd.Run()
if err != nil {
return err
}
return nil
}
func (t *Torrent) PublishTorrent(torrentPath string, savePath string) error {
qbURL := t.Settings.Torrent.QbUrl
username := t.Settings.Torrent.QbUsername
password := t.Settings.Torrent.QbPassword
client := &http.Client{}
sid, err := login(client, qbURL, username, password)
if err != nil {
return fmt.Errorf("failed to login: %v", err)
}
err = addTorrent(client, qbURL, sid, torrentPath, savePath)
if err != nil {
return fmt.Errorf("failed to add torrent: %v", err)
}
return nil
}
func (t *Torrent) GetDefaultCreateBody(createBody users.CreateTorrentBody) (*users.CreateTorrentBody, error) {
announces, err := fetchTrackerList(t.Settings.Torrent.TrackersListUrl)
if err != nil {
return nil, fmt.Errorf("failed to fetch tracker list: %v", err)
}
if createBody.Announces == nil {
createBody.Announces = []string{}
}
if createBody.WebSeeds == nil {
createBody.WebSeeds = []string{}
}
return &users.CreateTorrentBody{
Announces: announces,
Comment: createBody.Comment,
Date: createBody.Date,
Name: "",
PieceLen: createBody.PieceLen,
Private: createBody.Private,
Source: createBody.Source,
WebSeeds: createBody.WebSeeds,
}, nil
}
func fetchTrackerList(url string) ([]string, error) {
// 发送HTTP GET请求
response, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch trackers: %v", err)
}
defer response.Body.Close()
// 检查响应状态
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch trackers: status code %d", response.StatusCode)
}
// 读取响应体
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
// 移除空行
text := string(body)
text = strings.TrimSpace(text)
lines := strings.Split(text, "\n")
var filteredLines []string
for _, line := range lines {
if strings.TrimSpace(line) != "" {
filteredLines = append(filteredLines, line)
}
}
return filteredLines, nil
}

12
users/torrent.go Normal file
View File

@ -0,0 +1,12 @@
package users
type CreateTorrentBody struct {
Announces []string `json:"announces"`
Comment string `json:"comment"`
Date bool `json:"date"`
Name string `json:"name"`
PieceLen int `json:"pieceLen"`
Private bool `json:"private"`
Source string `json:"source"`
WebSeeds []string `json:"webSeeds"`
}

View File

@ -21,22 +21,22 @@ const (
// User describes a user. // User describes a user.
type User struct { type User struct {
ID uint `storm:"id,increment" json:"id"` ID uint `storm:"id,increment" json:"id"`
Username string `storm:"unique" json:"username"` Username string `storm:"unique" json:"username"`
Password string `json:"password"` Password string `json:"password"`
Scope string `json:"scope"` Scope string `json:"scope"`
Locale string `json:"locale"` Locale string `json:"locale"`
LockPassword bool `json:"lockPassword"` LockPassword bool `json:"lockPassword"`
ViewMode ViewMode `json:"viewMode"` ViewMode ViewMode `json:"viewMode"`
SingleClick bool `json:"singleClick"` SingleClick bool `json:"singleClick"`
Perm Permissions `json:"perm"` Perm Permissions `json:"perm"`
Commands []string `json:"commands"` Commands []string `json:"commands"`
Sorting files.Sorting `json:"sorting"` Sorting files.Sorting `json:"sorting"`
Fs afero.Fs `json:"-" yaml:"-"` Fs afero.Fs `json:"-" yaml:"-"`
Rules []rules.Rule `json:"rules"` Rules []rules.Rule `json:"rules"`
HideDotfiles bool `json:"hideDotfiles"` HideDotfiles bool `json:"hideDotfiles"`
DateFormat bool `json:"dateFormat"` DateFormat bool `json:"dateFormat"`
TrackerListsUrl string `json:"trackerListsUrl"` CreateBody CreateTorrentBody `json:"createBody"`
} }
// GetRules implements rules.Provider. // GetRules implements rules.Provider.