feature: add global.torrent settings card
feature: remember make torrent optionspull/3671/head
parent
9ff78ded0e
commit
1d1cc4b7f8
|
@ -17,4 +17,4 @@ EXPOSE 80
|
|||
COPY docker_config.json /.filebrowser.json
|
||||
COPY filebrowser /filebrowser
|
||||
|
||||
ENTRYPOINT [ "/filebrowser" ]
|
||||
ENTRYPOINT [ "/filebrowser" ]
|
||||
|
|
|
@ -14,4 +14,4 @@ COPY filebrowser /usr/bin/filebrowser
|
|||
|
||||
# ports and volumes
|
||||
VOLUME /srv /config /database
|
||||
EXPOSE 80
|
||||
EXPOSE 80
|
||||
|
|
|
@ -337,6 +337,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
|
|||
Download: true,
|
||||
Torrent: true,
|
||||
},
|
||||
CreateBody: users.CreateTorrentBody{
|
||||
Date: true,
|
||||
},
|
||||
},
|
||||
AuthMethod: "",
|
||||
Branding: settings.Branding{},
|
||||
|
|
|
@ -78,7 +78,7 @@ func addUserFlags(flags *pflag.FlagSet) {
|
|||
flags.String("locale", "en", "locale for users")
|
||||
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
|
||||
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 {
|
||||
|
|
|
@ -48,6 +48,7 @@ options you want to change.`,
|
|||
Perm: user.Perm,
|
||||
Sorting: user.Sorting,
|
||||
Commands: user.Commands,
|
||||
CreateBody: user.CreateBody,
|
||||
}
|
||||
getUserDefaults(flags, &defaults, false)
|
||||
user.Scope = defaults.Scope
|
||||
|
@ -57,6 +58,7 @@ options you want to change.`,
|
|||
user.Perm = defaults.Perm
|
||||
user.Commands = defaults.Commands
|
||||
user.Sorting = defaults.Sorting
|
||||
user.CreateBody = defaults.CreateBody
|
||||
user.LockPassword = mustGetBool(flags, "lockPassword")
|
||||
|
||||
if newUsername != "" {
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { fetchURL, fetchJSON, removePrefix, createURL } from "./utils";
|
||||
|
||||
export async function fetchDefaultOptions() {
|
||||
const url = `/api/torrent`
|
||||
return fetchJSON(url);
|
||||
}
|
||||
|
||||
export async function makeTorrent(
|
||||
url: string,
|
||||
announces: string[],
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p>
|
||||
{{ $t("prompts.publishMessageSingle") }}
|
||||
{{ $t("prompts.publishMessage") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
|
@ -65,9 +65,9 @@ export default {
|
|||
this.req.items[this.selected[0]].url
|
||||
).then(() => {
|
||||
buttons.success("publish");
|
||||
this.$showSuccess(this.$t("prompts.publishSuccess"));
|
||||
this.$showSuccess(this.$t("success.torrentPublished"));
|
||||
}).catch((e) => {
|
||||
buttons.done("delete");
|
||||
buttons.done("publish");
|
||||
this.$showError(e);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -2,44 +2,85 @@
|
|||
<div class="card floating" style="max-width: 50em;" id="share">
|
||||
<div class="card-title">
|
||||
<h2>{{ $t("buttons.torrent") }}</h2>
|
||||
<button class="button button--flat button--blue" @click="toggleDetailedView">
|
||||
{{ detailedView ? $t("buttons.compact") : $t("buttons.detailed") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<textarea
|
||||
class="input input--block input--textarea"
|
||||
style="min-height: 5em;"
|
||||
style="min-height: 4em;"
|
||||
type="text"
|
||||
v-model.trim="announces"
|
||||
tabindex="1"></textarea>
|
||||
tabindex="2"></textarea>
|
||||
|
||||
<p>{{ $t("prompts.webSeeds") }}</p>
|
||||
<textarea
|
||||
<textarea
|
||||
class="input input--block input--textarea"
|
||||
style="min-height: 5em;"
|
||||
style="min-height: 4em;"
|
||||
type="text"
|
||||
v-model.trim="webSeeds"
|
||||
tabindex="2"></textarea>
|
||||
tabindex="3"></textarea>
|
||||
|
||||
<p>{{ $t("prompts.comment") }}</p>
|
||||
<textarea
|
||||
class="input input--block input--textarea"
|
||||
style="min-height: 5em;"
|
||||
style="min-height: 4em;"
|
||||
type="text"
|
||||
v-model.trim="comment"
|
||||
tabindex="3"></textarea>
|
||||
tabindex="4"></textarea>
|
||||
|
||||
<p>{{ $t("prompts.source") }}</p>
|
||||
<p v-if="detailedView">{{ $t("prompts.source") }}</p>
|
||||
<input
|
||||
v-if="detailedView"
|
||||
class="input input--block"
|
||||
type="text"
|
||||
v-model.trim="source"
|
||||
tabindex="4" />
|
||||
tabindex="5" />
|
||||
|
||||
<label>
|
||||
<input type="checkbox" v-model="privateFlag" tabindex="3" />
|
||||
<p v-if="detailedView">
|
||||
<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") }}
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
|
@ -70,10 +111,11 @@ export default {
|
|||
comment: "",
|
||||
date: true,
|
||||
name: "",
|
||||
pieceLen: 18,
|
||||
pieceLen: 0,
|
||||
privateFlag: false,
|
||||
source: "",
|
||||
webSeeds: [],
|
||||
detailedView: false
|
||||
};
|
||||
},
|
||||
inject: ["$showError", "$showSuccess"],
|
||||
|
@ -99,25 +141,21 @@ export default {
|
|||
},
|
||||
},
|
||||
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: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
async fetchTrackers() {
|
||||
const url = 'https://cf.trackerslist.com/all.txt';
|
||||
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;
|
||||
}
|
||||
toggleDetailedView() {
|
||||
this.detailedView = !this.detailedView;
|
||||
},
|
||||
torrent: async function (event) {
|
||||
event.preventDefault();
|
||||
|
@ -135,10 +173,10 @@ export default {
|
|||
this.comment,
|
||||
this.date,
|
||||
this.name,
|
||||
this.pieceLen,
|
||||
parseInt(this.pieceLen),
|
||||
this.privateFlag,
|
||||
this.source,
|
||||
this.webSeeds,
|
||||
this.webSeeds.split("\n").map((t) => t.trim()).filter((t) => t),
|
||||
).then(
|
||||
() => {
|
||||
buttons.success("torrent");
|
||||
|
@ -147,9 +185,9 @@ export default {
|
|||
this.$showSuccess(this.$t("success.torrentCreated"));
|
||||
}
|
||||
).catch((e) => {
|
||||
buttons.done("torrent");
|
||||
this.$showError(e);
|
||||
});
|
||||
buttons.done("torrent");
|
||||
this.$showError(e);
|
||||
});
|
||||
}
|
||||
|
||||
this.closeHovers();
|
||||
|
|
|
@ -42,7 +42,9 @@
|
|||
"openFile": "打开文件",
|
||||
"continue": "继续",
|
||||
"fullScreen": "切换全屏",
|
||||
"discardChanges": "放弃更改"
|
||||
"discardChanges": "放弃更改",
|
||||
"detailed": "转到详细视图",
|
||||
"compact": "转到精简视图"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下载文件",
|
||||
|
@ -144,11 +146,15 @@
|
|||
"resolution": "分辨率",
|
||||
"deleteUser": "你确定要删除这个用户吗?",
|
||||
"discardEditorChanges": "你确定要放弃所做的更改吗?",
|
||||
"pieceLength": "分片大小:",
|
||||
"auto": "自动",
|
||||
"trackersList": "Tracker URL:",
|
||||
"comment": "注释:",
|
||||
"webSeeds": "Web 种子 URL:",
|
||||
"source": "源:",
|
||||
"privateTorrent": "私有torrent(不会在DHT网络上分发)"
|
||||
"includeDate": "写入创建日期",
|
||||
"privateTorrent": "私有 torrent (不会在DHT网络上分发)",
|
||||
"publishMessage": "你确定要发布这个种子吗?"
|
||||
},
|
||||
"search": {
|
||||
"images": "图像",
|
||||
|
@ -245,7 +251,16 @@
|
|||
"userManagement": "用户管理",
|
||||
"userUpdated": "用户已更新!",
|
||||
"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": {
|
||||
"help": "帮助",
|
||||
|
@ -262,7 +277,8 @@
|
|||
},
|
||||
"success": {
|
||||
"linkCopied": "链接已复制!",
|
||||
"torrentCreated": "种子已创建!"
|
||||
"torrentCreated": "种子已创建!",
|
||||
"torrentPublished": "种子已发布!"
|
||||
},
|
||||
"time": {
|
||||
"days": "天",
|
||||
|
|
|
@ -8,6 +8,7 @@ interface ISettings {
|
|||
tus: SettingsTus;
|
||||
shell: string[];
|
||||
commands: SettingsCommand;
|
||||
torrent: SettingsTorrent;
|
||||
}
|
||||
|
||||
interface SettingsDefaults {
|
||||
|
@ -49,6 +50,13 @@ interface SettingsCommand {
|
|||
before_upload?: string[];
|
||||
}
|
||||
|
||||
interface SettingsTorrent {
|
||||
trackersListUrl?: string;
|
||||
qbUrl?: string;
|
||||
qbUsername?: string;
|
||||
qbPassword?: string;
|
||||
}
|
||||
|
||||
interface SettingsUnit {
|
||||
KB: number;
|
||||
MB: number;
|
||||
|
|
|
@ -225,6 +225,67 @@
|
|||
</div>
|
||||
</form>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/filebrowser/filebrowser/v2/runner"
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/storage"
|
||||
"github.com/filebrowser/filebrowser/v2/torrent"
|
||||
"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 {
|
||||
*runner.Runner
|
||||
*torrent.Torrent
|
||||
settings *settings.Settings
|
||||
server *settings.Server
|
||||
store *storage.Storage
|
||||
|
@ -61,6 +63,7 @@ func handle(fn handleFunc, prefix string, store *storage.Storage, server *settin
|
|||
|
||||
status, err := fn(w, r, &data{
|
||||
Runner: &runner.Runner{Enabled: server.EnableExec, Settings: settings},
|
||||
Torrent: &torrent.Torrent{Settings: settings},
|
||||
store: store,
|
||||
settings: settings,
|
||||
server: server,
|
||||
|
|
|
@ -78,6 +78,7 @@ func NewHandler(
|
|||
api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST")
|
||||
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("/publish").Handler(monkey(publishPostHandler, "/api/publish")).Methods("POST")
|
||||
|
||||
|
|
|
@ -18,10 +18,11 @@ type settingsData struct {
|
|||
Tus settings.Tus `json:"tus"`
|
||||
Shell []string `json:"shell"`
|
||||
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) {
|
||||
data := &settingsData{
|
||||
data := settingsData{
|
||||
Signup: d.settings.Signup,
|
||||
CreateUserDir: d.settings.CreateUserDir,
|
||||
UserHomeBasePath: d.settings.UserHomeBasePath,
|
||||
|
@ -31,8 +32,11 @@ var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request,
|
|||
Tus: d.settings.Tus,
|
||||
Shell: d.settings.Shell,
|
||||
Commands: d.settings.Commands,
|
||||
Torrent: d.settings.Torrent,
|
||||
}
|
||||
|
||||
data.Torrent.QbPassword = ""
|
||||
|
||||
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.Shell = req.Shell
|
||||
d.settings.Commands = req.Commands
|
||||
d.settings.Torrent = req.Torrent
|
||||
|
||||
err = d.store.Settings.Save(d.settings)
|
||||
return errToStatus(err), err
|
||||
|
|
153
http/torrent.go
153
http/torrent.go
|
@ -4,11 +4,10 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/files"
|
||||
"github.com/filebrowser/filebrowser/v2/torrent"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
func withPermTorrent(fn handleFunc) handleFunc {
|
||||
|
@ -21,97 +20,15 @@ func withPermTorrent(fn handleFunc) handleFunc {
|
|||
})
|
||||
}
|
||||
|
||||
/*
|
||||
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)
|
||||
var torrentGetHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
// return default torrent options
|
||||
s, err := d.GetDefaultCreateBody(d.user.CreateBody)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to get default create body: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return renderJSON(w, r, s)
|
||||
})
|
||||
|
||||
var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
file, err := files.NewFileInfo(&files.FileOptions{
|
||||
|
@ -128,8 +45,7 @@ var torrentPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Req
|
|||
}
|
||||
fPath := file.RealPath()
|
||||
|
||||
var s *torrent.Torrent
|
||||
var body torrent.CreateBody
|
||||
var body users.CreateTorrentBody
|
||||
if r.Body != nil {
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
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()
|
||||
}
|
||||
|
||||
// 设置 mktorrent 命令的选项
|
||||
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()
|
||||
|
||||
err = d.Torrent.MakeTorrent(fPath, body)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
s = &torrent.Torrent{
|
||||
Path: fPath + ".torrent",
|
||||
d.user.CreateBody = body
|
||||
|
||||
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) {
|
||||
|
@ -186,25 +85,13 @@ var publishPostHandler = withPermTorrent(func(w http.ResponseWriter, r *http.Req
|
|||
// only folder path
|
||||
fPath := filepath.Dir(tPath)
|
||||
|
||||
qbURL := "http://localhost:8081" // 修改为你的qBittorrent URL
|
||||
username := "moezakura" // 修改为你的用户名
|
||||
password := "moezakura" // 修改为你的密码
|
||||
torrentPath := tPath // 修改为你的本地torrent文件路径
|
||||
savePath := fPath // 修改为你的保存路径
|
||||
torrentPath := tPath
|
||||
savePath := fPath
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
sid, err := torrent.Login(client, qbURL, username, password)
|
||||
err = d.Torrent.PublishTorrent(torrentPath, savePath)
|
||||
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)
|
||||
})
|
||||
|
|
|
@ -8,16 +8,16 @@ import (
|
|||
// UserDefaults is a type that holds the default values
|
||||
// for some fields on User.
|
||||
type UserDefaults struct {
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode users.ViewMode `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Sorting files.Sorting `json:"sorting"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
TrackerListsUrl string `json:"trackerListsUrl"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
ViewMode users.ViewMode `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Sorting files.Sorting `json:"sorting"`
|
||||
Perm users.Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
CreateBody users.CreateTorrentBody `json:"createBody"`
|
||||
}
|
||||
|
||||
// Apply applies the default options to a user.
|
||||
|
@ -31,5 +31,5 @@ func (d *UserDefaults) Apply(u *users.User) {
|
|||
u.Commands = d.Commands
|
||||
u.HideDotfiles = d.HideDotfiles
|
||||
u.DateFormat = d.DateFormat
|
||||
u.TrackerListsUrl = d.TrackerListsUrl
|
||||
u.CreateBody = d.CreateBody
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ type Settings struct {
|
|||
Commands map[string][]string `json:"commands"`
|
||||
Shell []string `json:"shell"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
Torrent Torrent `json:"torrent"`
|
||||
}
|
||||
|
||||
// GetRules implements rules.Provider.
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -135,6 +135,9 @@ func importConf(db *storm.DB, path string, sto *storage.Storage) error {
|
|||
Share: true,
|
||||
Download: true,
|
||||
},
|
||||
CreateBody: users.CreateTorrentBody{
|
||||
Date: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -10,9 +10,10 @@ import (
|
|||
"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)
|
||||
data := fmt.Sprintf("username=%s&password=%s", username, password)
|
||||
|
||||
req, err := http.NewRequest("POST", loginURL, bytes.NewBufferString(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -39,7 +40,7 @@ func Login(client *http.Client, qbURL, username, password string) (string, error
|
|||
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)
|
||||
|
||||
file, err := os.Open(torrentPath)
|
||||
|
|
|
@ -1,30 +1,124 @@
|
|||
package torrent
|
||||
|
||||
type CreateBody 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"`
|
||||
}
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/settings"
|
||||
"github.com/filebrowser/filebrowser/v2/users"
|
||||
)
|
||||
|
||||
type Torrent struct {
|
||||
Path string `json:"Path"`
|
||||
*settings.Settings
|
||||
*users.User
|
||||
}
|
||||
|
||||
// Link is the information needed to build a shareable link.
|
||||
// type Torrent struct {
|
||||
// Hash string `json:"hash" storm:"id,index"`
|
||||
// Path string `json:"path" storm:"index"`
|
||||
// UserID uint `json:"userID"`
|
||||
// Expire int64 `json:"expire"`
|
||||
// PasswordHash string `json:"password_hash,omitempty"`
|
||||
// // Token is a random value that will only be set when PasswordHash is set. It is
|
||||
// // URL-Safe and is used to download links in password-protected shares via a
|
||||
// // query arg.
|
||||
// Token string `json:"token,omitempty"`
|
||||
// }
|
||||
//
|
||||
func (t *Torrent) MakeTorrent(fPath string, body users.CreateTorrentBody) error {
|
||||
tPath := fPath + ".torrent"
|
||||
|
||||
// 设置 mktorrent 命令的选项
|
||||
opts := &TorrentOptions{
|
||||
Target: fPath,
|
||||
Announces: body.Announces,
|
||||
Comment: body.Comment,
|
||||
Date: body.Date,
|
||||
Name: body.Name,
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -21,22 +21,22 @@ const (
|
|||
|
||||
// User describes a user.
|
||||
type User struct {
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode ViewMode `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting files.Sorting `json:"sorting"`
|
||||
Fs afero.Fs `json:"-" yaml:"-"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
TrackerListsUrl string `json:"trackerListsUrl"`
|
||||
ID uint `storm:"id,increment" json:"id"`
|
||||
Username string `storm:"unique" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Scope string `json:"scope"`
|
||||
Locale string `json:"locale"`
|
||||
LockPassword bool `json:"lockPassword"`
|
||||
ViewMode ViewMode `json:"viewMode"`
|
||||
SingleClick bool `json:"singleClick"`
|
||||
Perm Permissions `json:"perm"`
|
||||
Commands []string `json:"commands"`
|
||||
Sorting files.Sorting `json:"sorting"`
|
||||
Fs afero.Fs `json:"-" yaml:"-"`
|
||||
Rules []rules.Rule `json:"rules"`
|
||||
HideDotfiles bool `json:"hideDotfiles"`
|
||||
DateFormat bool `json:"dateFormat"`
|
||||
CreateBody CreateTorrentBody `json:"createBody"`
|
||||
}
|
||||
|
||||
// GetRules implements rules.Provider.
|
||||
|
|
Loading…
Reference in New Issue