remove distribyted ui
|
@ -1,4 +1,4 @@
|
|||
FROM --platform=$BUILDPLATFORM golang:1.22 as builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -9,16 +9,10 @@ RUN --mount=type=cache,mode=0777,target=/go/pkg/mod go mod download all
|
|||
COPY ./pkg ./pkg
|
||||
COPY ./src ./src
|
||||
COPY ./cmd ./cmd
|
||||
COPY ./assets ./assets
|
||||
COPY ./templates ./templates
|
||||
COPY embed.go embed.go
|
||||
|
||||
RUN go generate ./...
|
||||
|
||||
ARG TARGETOS TARGETARCH
|
||||
RUN --mount=type=cache,mode=0777,target=/go/pkg/mod CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -tags timetzdata -o /tstor ./cmd/tstor/main.go
|
||||
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
|
6
assets/css/sleek.min.css
vendored
Before Width: | Height: | Size: 1.8 KiB |
|
@ -1,48 +0,0 @@
|
|||
function CacheChart(id, name) {
|
||||
var ctx = document.getElementById(id).getContext('2d');
|
||||
this._chart = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["used", "free"],
|
||||
datasets: [
|
||||
{
|
||||
label: ["used", "free"],
|
||||
data: [0, 0],
|
||||
backgroundColor: ["#4c84ff", "#8061ef"],
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
animation: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
cutoutPercentage: 75,
|
||||
tooltips: {
|
||||
titleFontColor: "#888",
|
||||
bodyFontColor: "#555",
|
||||
titleFontSize: 12,
|
||||
bodyFontSize: 14,
|
||||
backgroundColor: "rgba(256,256,256,0.95)",
|
||||
displayColors: true,
|
||||
borderColor: "rgba(220, 220, 220, 0.9)",
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.update = function (used, free) {
|
||||
this._chart.data.datasets.forEach((dataset) => {
|
||||
dataset.data[0] = used;
|
||||
if (free < 0) {
|
||||
free = 0;
|
||||
}
|
||||
dataset.data[1] = free;
|
||||
});
|
||||
|
||||
this._chart.update();
|
||||
};
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
Handlebars.registerHelper("ibytes", function (bytesSec, timePassed) {
|
||||
return Humanize.ibytes(bytesSec / timePassed, 1024);
|
||||
});
|
||||
Handlebars.registerHelper("bytes", function (bytes) {
|
||||
return Humanize.bytes(bytes, 1024);
|
||||
});
|
||||
|
||||
var tstor = tstor || {};
|
||||
|
||||
tstor.message = {
|
||||
_toastr: function () {
|
||||
toastr.options = {
|
||||
closeButton: true,
|
||||
debug: false,
|
||||
newestOnTop: false,
|
||||
progressBar: true,
|
||||
positionClass: "toast-top-right",
|
||||
preventDuplicates: false,
|
||||
onclick: null,
|
||||
showDuration: "300",
|
||||
hideDuration: "1000",
|
||||
timeOut: "5000",
|
||||
extendedTimeOut: "1000",
|
||||
showEasing: "swing",
|
||||
hideEasing: "linear",
|
||||
showMethod: "fadeIn",
|
||||
hideMethod: "fadeOut",
|
||||
};
|
||||
|
||||
return toastr;
|
||||
},
|
||||
|
||||
error: function (message) {
|
||||
this._toastr().error(message);
|
||||
},
|
||||
|
||||
info: function (message) {
|
||||
this._toastr().info(message);
|
||||
},
|
||||
};
|
||||
|
||||
$(document).ready(function () {
|
||||
"use strict";
|
||||
|
||||
/*======== 1. SCROLLBAR SIDEBAR ========*/
|
||||
var sidebarScrollbar = $(".sidebar-scrollbar");
|
||||
if (sidebarScrollbar.length != 0) {
|
||||
sidebarScrollbar
|
||||
.slimScroll({
|
||||
opacity: 0,
|
||||
height: "100%",
|
||||
color: "#808080",
|
||||
size: "5px",
|
||||
touchScrollStep: 50,
|
||||
})
|
||||
.mouseover(function () {
|
||||
$(this).next(".slimScrollBar").css("opacity", 0.5);
|
||||
});
|
||||
}
|
||||
|
||||
/*======== 2. MOBILE OVERLAY ========*/
|
||||
if ($(window).width() < 768) {
|
||||
$(".sidebar-toggle").on("click", function () {
|
||||
$("body").css("overflow", "hidden");
|
||||
$("body").prepend('<div class="mobile-sticky-body-overlay"></div>');
|
||||
});
|
||||
|
||||
$(document).on("click", ".mobile-sticky-body-overlay", function (e) {
|
||||
$(this).remove();
|
||||
$("#body")
|
||||
.removeClass("sidebar-mobile-in")
|
||||
.addClass("sidebar-mobile-out");
|
||||
$("body").css("overflow", "auto");
|
||||
});
|
||||
}
|
||||
|
||||
/*======== 3. SIDEBAR MENU ========*/
|
||||
var sidebar = $(".sidebar");
|
||||
if (sidebar.length != 0) {
|
||||
$(".sidebar .nav > .has-sub > a").click(function () {
|
||||
$(this).parent().siblings().removeClass("expand");
|
||||
$(this).parent().toggleClass("expand");
|
||||
});
|
||||
|
||||
$(".sidebar .nav > .has-sub .has-sub > a").click(function () {
|
||||
$(this).parent().toggleClass("expand");
|
||||
});
|
||||
}
|
||||
|
||||
/*======== 4. SIDEBAR TOGGLE FOR MOBILE ========*/
|
||||
if ($(window).width() < 768) {
|
||||
$(document).on("click", ".sidebar-toggle", function (e) {
|
||||
e.preventDefault();
|
||||
var min = "sidebar-mobile-in",
|
||||
min_out = "sidebar-mobile-out",
|
||||
body = "#body";
|
||||
$(body).hasClass(min)
|
||||
? $(body).removeClass(min).addClass(min_out)
|
||||
: $(body).addClass(min).removeClass(min_out);
|
||||
});
|
||||
}
|
||||
|
||||
/*======== 5. SIDEBAR TOGGLE FOR VARIOUS SIDEBAR LAYOUT ========*/
|
||||
var body = $("#body");
|
||||
if ($(window).width() >= 768) {
|
||||
if (typeof window.isMinified === "undefined") {
|
||||
window.isMinified = false;
|
||||
}
|
||||
if (typeof window.isCollapsed === "undefined") {
|
||||
window.isCollapsed = false;
|
||||
}
|
||||
|
||||
$("#sidebar-toggler").on("click", function () {
|
||||
if (
|
||||
body.hasClass("sidebar-fixed-offcanvas") ||
|
||||
body.hasClass("sidebar-static-offcanvas")
|
||||
) {
|
||||
$(this)
|
||||
.addClass("sidebar-offcanvas-toggle")
|
||||
.removeClass("sidebar-toggle");
|
||||
if (window.isCollapsed === false) {
|
||||
body.addClass("sidebar-collapse");
|
||||
window.isCollapsed = true;
|
||||
window.isMinified = false;
|
||||
} else {
|
||||
body.removeClass("sidebar-collapse");
|
||||
body.addClass("sidebar-collapse-out");
|
||||
setTimeout(function () {
|
||||
body.removeClass("sidebar-collapse-out");
|
||||
}, 300);
|
||||
window.isCollapsed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (body.hasClass("sidebar-fixed") || body.hasClass("sidebar-static")) {
|
||||
$(this)
|
||||
.addClass("sidebar-toggle")
|
||||
.removeClass("sidebar-offcanvas-toggle");
|
||||
if (window.isMinified === false) {
|
||||
body
|
||||
.removeClass("sidebar-collapse sidebar-minified-out")
|
||||
.addClass("sidebar-minified");
|
||||
window.isMinified = true;
|
||||
window.isCollapsed = false;
|
||||
} else {
|
||||
body.removeClass("sidebar-minified");
|
||||
body.addClass("sidebar-minified-out");
|
||||
window.isMinified = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($(window).width() >= 768 && $(window).width() < 992) {
|
||||
if (body.hasClass("sidebar-fixed") || body.hasClass("sidebar-static")) {
|
||||
body
|
||||
.removeClass("sidebar-collapse sidebar-minified-out")
|
||||
.addClass("sidebar-minified");
|
||||
window.isMinified = true;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,160 +0,0 @@
|
|||
tstor.config = {
|
||||
_editor: null,
|
||||
_infoDiv: document.getElementById("tstor-reload-info-text"),
|
||||
_loadingInfoDom: document.getElementById("tstor-reload-info-loading"),
|
||||
_valid: function () {
|
||||
if (this._editor == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let getYamlCodeValidationErrors = (code) => {
|
||||
var error = "";
|
||||
try {
|
||||
jsyaml.safeLoad(code);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
let code = this._editor.getValue();
|
||||
let error = getYamlCodeValidationErrors(code);
|
||||
if (error) {
|
||||
this._editor.getSession().setAnnotations([
|
||||
{
|
||||
row: error.mark.line,
|
||||
column: error.mark.column,
|
||||
text: error.reason,
|
||||
type: "error",
|
||||
},
|
||||
]);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
this._editor.getSession().setAnnotations([]);
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
save: function () {
|
||||
fetch("/api/config", {
|
||||
method: "POST",
|
||||
body: this._editor.getValue(),
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
tstor.message.info("Configuration saved");
|
||||
} else {
|
||||
tstor.message.error(
|
||||
"Error saving configuration file. Response: " + response.status
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error(
|
||||
"Error saving configuration file: " + error.message
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
reload: function () {
|
||||
this.cleanInfo();
|
||||
fetch("/api/reload", {
|
||||
method: "POST",
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
tstor.config.showInfo(
|
||||
"Error reloading server. Response: " + response.status,
|
||||
"ko"
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(function (json) {
|
||||
tstor.config.showInfo(json.message, "ok");
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error("Error reloading server: " + error.message);
|
||||
});
|
||||
},
|
||||
|
||||
cleanInfo: function () {
|
||||
this._loadingInfoDom.style.display = "block";
|
||||
this._infoDiv.innerText = "";
|
||||
},
|
||||
|
||||
showInfo: function (message, flag) {
|
||||
const li = document.createElement("li");
|
||||
li.innerText = message;
|
||||
li.className = "list-group-item";
|
||||
if (flag == "ok") {
|
||||
li.className += " list-group-item-success";
|
||||
} else if (flag == "ko") {
|
||||
li.className += " list-group-item-danger";
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
this._loadingInfoDom.style.display = "none";
|
||||
}
|
||||
|
||||
this._infoDiv.appendChild(li);
|
||||
},
|
||||
|
||||
loadView: function () {
|
||||
this._editor = ace.edit("editor");
|
||||
this._editor.getSession().setMode("ace/mode/yaml");
|
||||
this._editor.setShowPrintMargin(false);
|
||||
this._editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: false,
|
||||
|
||||
autoScrollEditorIntoView: true,
|
||||
fontSize: "16px",
|
||||
maxLines: 100,
|
||||
wrap: true,
|
||||
});
|
||||
|
||||
this._editor.commands.addCommand({
|
||||
name: "save",
|
||||
bindKey: { win: "Ctrl-S", mac: "Command-S" },
|
||||
exec: function (editor) {
|
||||
if (tstor.config._valid()) {
|
||||
tstor.config.save();
|
||||
} else {
|
||||
tstor.message.error("Check file format errors before saving");
|
||||
}
|
||||
},
|
||||
readOnly: false,
|
||||
});
|
||||
|
||||
this._editor.on("change", () => {
|
||||
tstor.config._valid();
|
||||
});
|
||||
|
||||
fetch("/api/config")
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
} else {
|
||||
tstor.message.error(
|
||||
"Error getting data from server. Response: " + response.status
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(function (yaml) {
|
||||
tstor.config._editor.setValue(yaml);
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error("Error getting yaml from server: " + error.message);
|
||||
});
|
||||
|
||||
var stream = new EventSource("/api/events");
|
||||
stream.addEventListener("event", function (e) {
|
||||
tstor.config.showInfo(e.data);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,39 +0,0 @@
|
|||
GeneralChart.init();
|
||||
|
||||
tstor.dashboard = {
|
||||
_cacheChart: new CacheChart("main-cache-chart", "Cache disk"),
|
||||
loadView: function () {
|
||||
fetch("/api/status")
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
tstor.message.error(
|
||||
"Error getting data from server. Response: " + response.status
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(function (stats) {
|
||||
var download =
|
||||
stats.torrentStats.downloadedBytes / stats.torrentStats.timePassed;
|
||||
var upload =
|
||||
stats.torrentStats.uploadedBytes / stats.torrentStats.timePassed;
|
||||
|
||||
GeneralChart.update(download, upload);
|
||||
|
||||
tstor.dashboard._cacheChart.update(
|
||||
stats.cacheFilled,
|
||||
stats.cacheCapacity - stats.cacheFilled
|
||||
);
|
||||
|
||||
document.getElementById("general-download-speed").innerText =
|
||||
Humanize.ibytes(download, 1024) + "/s";
|
||||
|
||||
document.getElementById("general-upload-speed").innerText =
|
||||
Humanize.ibytes(upload, 1024) + "/s";
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error("Error getting status info: " + error.message);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,138 +0,0 @@
|
|||
var GeneralChart = {
|
||||
_downloadData: [],
|
||||
_uploadData: [],
|
||||
_chart: null,
|
||||
update: function (download, upload) {
|
||||
if (this._downloadData.length > 20) {
|
||||
this._uploadData.shift();
|
||||
this._downloadData.shift();
|
||||
}
|
||||
var date = new Date();
|
||||
this._downloadData.push({
|
||||
x: date,
|
||||
y: download,
|
||||
});
|
||||
this._uploadData.push({
|
||||
x: date,
|
||||
y: upload,
|
||||
});
|
||||
this._chart.update();
|
||||
},
|
||||
init: function () {
|
||||
var domElem = document.getElementById('chart-general-network')
|
||||
domElem.height = 300;
|
||||
var ctx = domElem.getContext('2d');
|
||||
this._chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Download Speed',
|
||||
fill: false,
|
||||
backgroundColor: "transparent",
|
||||
borderColor: "rgb(82, 136, 255)",
|
||||
|
||||
lineTension: 0.3,
|
||||
pointRadius: 5,
|
||||
pointBackgroundColor: "rgba(255,255,255,1)",
|
||||
pointHoverBackgroundColor: "rgba(255,255,255,1)",
|
||||
pointBorderWidth: 2,
|
||||
pointHoverRadius: 8,
|
||||
pointHoverBorderWidth: 1,
|
||||
|
||||
data: this._downloadData,
|
||||
|
||||
},
|
||||
{
|
||||
label: 'Upload Speed',
|
||||
fill: false,
|
||||
backgroundColor: "transparent",
|
||||
borderColor: "rgb(82, 136, 180)",
|
||||
|
||||
lineTension: 0.3,
|
||||
pointRadius: 5,
|
||||
pointBackgroundColor: "rgba(255,255,255,1)",
|
||||
pointHoverBackgroundColor: "rgba(255,255,255,1)",
|
||||
pointBorderWidth: 2,
|
||||
pointHoverRadius: 8,
|
||||
pointHoverBorderWidth: 1,
|
||||
|
||||
data: this._uploadData,
|
||||
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
right: 10
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: 'Download and Upload speed'
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
scaleLabel: {
|
||||
display: false,
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
type: 'time',
|
||||
}],
|
||||
yAxes: [{
|
||||
scaleLabel: {
|
||||
display: false,
|
||||
color: "#eee",
|
||||
zeroLineColor: "#eee",
|
||||
},
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
userCallback: function (tick) {
|
||||
return Humanize.ibytes(tick, 1024) + "/s";
|
||||
},
|
||||
beginAtZero: true
|
||||
},
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, data) {
|
||||
var label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
|
||||
return Humanize.ibytes(tooltipItem.yLabel, 1024) + "/s";
|
||||
}
|
||||
},
|
||||
responsive: true,
|
||||
intersect: false,
|
||||
enabled: true,
|
||||
titleFontColor: "#888",
|
||||
bodyFontColor: "#555",
|
||||
titleFontSize: 12,
|
||||
bodyFontSize: 18,
|
||||
backgroundColor: "rgba(256,256,256,0.95)",
|
||||
xPadding: 20,
|
||||
yPadding: 10,
|
||||
displayColors: false,
|
||||
borderColor: "rgba(220, 220, 220, 0.9)",
|
||||
borderWidth: 2,
|
||||
caretSize: 10,
|
||||
caretPadding: 15
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
var isizes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||
var sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
|
||||
function logn(n, b) {
|
||||
return Math.log(n) / Math.log(b);
|
||||
}
|
||||
|
||||
var Humanize = {
|
||||
bytes: function (s, base) {
|
||||
if (s < 10) {
|
||||
return s.toFixed(0) + " B";
|
||||
}
|
||||
var e = Math.floor(logn(s, base));
|
||||
var suffix = sizes[e];
|
||||
var val = Math.floor(s / Math.pow(base, e) * 10 + 0.5) / 10;
|
||||
|
||||
var f = val.toFixed(0);
|
||||
|
||||
if (val < 10) {
|
||||
f = val.toFixed(1);
|
||||
}
|
||||
|
||||
return f + suffix;
|
||||
},
|
||||
ibytes: function (s, base) {
|
||||
if (s < 10) {
|
||||
return s.toFixed(0) + " B";
|
||||
}
|
||||
var e = Math.floor(logn(s, base));
|
||||
var suffix = isizes[e];
|
||||
var val = Math.floor(s / Math.pow(base, e) * 10 + 0.5) / 10;
|
||||
|
||||
var f = val.toFixed(0);
|
||||
|
||||
if (val < 10) {
|
||||
f = val.toFixed(1);
|
||||
}
|
||||
|
||||
return f + suffix;
|
||||
}
|
||||
};
|
|
@ -1,88 +0,0 @@
|
|||
tstor.logs = {
|
||||
loadView: function () {
|
||||
fetch("/api/log")
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.body.getReader();
|
||||
} else {
|
||||
response
|
||||
.json()
|
||||
.then((json) => {
|
||||
tstor.message.error(
|
||||
"Error getting logs from server. Error: " + json.error
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
tstor.message.error(
|
||||
"Error getting logs from server. Error: " + error
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then((reader) => {
|
||||
var decoder = new TextDecoder();
|
||||
var lastString = "";
|
||||
reader
|
||||
.read()
|
||||
.then(function processText({ done, value }) {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
|
||||
const string = `${lastString}${decoder.decode(value)}`;
|
||||
const lines = string.split(/\r\n|[\r\n]/g);
|
||||
this.lastString = lines.pop() || "";
|
||||
|
||||
lines.forEach((element) => {
|
||||
try {
|
||||
var json = JSON.parse(element);
|
||||
var properties = "";
|
||||
for (let [key, value] of Object.entries(json)) {
|
||||
if (
|
||||
key == "level" ||
|
||||
key == "component" ||
|
||||
key == "message" ||
|
||||
key == "time"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
properties += `<b>${key}</b>=${value} `;
|
||||
}
|
||||
|
||||
var tableClass = "table-primary";
|
||||
switch (json.level) {
|
||||
case "info":
|
||||
tableClass = "";
|
||||
break;
|
||||
case "error":
|
||||
tableClass = "table-danger";
|
||||
break;
|
||||
case "warn":
|
||||
tableClass = "table-warning";
|
||||
break;
|
||||
case "debug":
|
||||
tableClass = "table-info";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
template = `<tr class="${tableClass}"><td>${new Date(
|
||||
json.time * 1000
|
||||
).toLocaleString()}</td><td>${json.level}</td><td>${
|
||||
json.component
|
||||
}</td><td>${json.message}</td><td>${properties}</td></tr>`;
|
||||
document.getElementById("log_table").innerHTML += template;
|
||||
} catch (err) {
|
||||
// server can send some corrupted json line
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
|
||||
return reader.read().then(processText);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
},
|
||||
};
|
|
@ -1,204 +0,0 @@
|
|||
Handlebars.registerHelper("torrent_status", function (chunks, totalPieces) {
|
||||
const pieceStatus = {
|
||||
H: { class: "bg-warning", tooltip: "checking pieces" },
|
||||
P: { class: "bg-info", tooltip: "" },
|
||||
C: { class: "bg-success", tooltip: "downloaded pieces" },
|
||||
W: { class: "bg-transparent" },
|
||||
"?": { class: "bg-danger", tooltip: "erroed pieces" },
|
||||
};
|
||||
const chunksAsHTML = chunks.map((chunk) => {
|
||||
const percentage = (totalPieces * chunk.numPieces) / 100;
|
||||
const pcMeta = pieceStatus[chunk.status];
|
||||
const pieceStatusClass = pcMeta.class;
|
||||
const pieceStatusTip = pcMeta.tooltip;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "progress-bar " + pieceStatusClass;
|
||||
div.setAttribute("role", "progressbar");
|
||||
|
||||
if (pieceStatusTip) {
|
||||
div.setAttribute("data-toggle", "tooltip");
|
||||
div.setAttribute("data-placement", "top");
|
||||
div.setAttribute("title", pieceStatusTip);
|
||||
}
|
||||
|
||||
div.style.cssText = "width: " + percentage + "%";
|
||||
|
||||
return div.outerHTML;
|
||||
});
|
||||
|
||||
return '<div class="progress mb-3">' + chunksAsHTML.join("\n");
|
||||
+"</div>";
|
||||
});
|
||||
|
||||
Handlebars.registerHelper("torrent_info", function (peers, seeders, pieceSize) {
|
||||
const MB = 1048576;
|
||||
|
||||
var messages = [];
|
||||
|
||||
var errorLevels = [];
|
||||
const seedersMsg = "- Number of seeders is too low (" + seeders + ").";
|
||||
if (seeders < 2) {
|
||||
errorLevels[0] = 2;
|
||||
messages.push(seedersMsg);
|
||||
} else if (seeders >= 2 && seeders < 4) {
|
||||
errorLevels[0] = 1;
|
||||
messages.push(seedersMsg);
|
||||
} else {
|
||||
errorLevels[0] = 0;
|
||||
}
|
||||
|
||||
const pieceSizeMsg =
|
||||
"- Piece size is too big (" +
|
||||
Humanize.bytes(pieceSize, 1024) +
|
||||
"). Recommended size is 1MB or less.";
|
||||
if (pieceSize <= MB) {
|
||||
errorLevels[1] = 0;
|
||||
} else if (pieceSize > MB && pieceSize < MB * 4) {
|
||||
errorLevels[1] = 1;
|
||||
messages.push(pieceSizeMsg);
|
||||
} else {
|
||||
errorLevels[2] = 2;
|
||||
messages.push(pieceSizeMsg);
|
||||
}
|
||||
|
||||
const level = ["text-success", "text-warning", "text-danger"];
|
||||
const icon = ["mdi-check", "mdi-alert", "mdi-alert-octagram"];
|
||||
const div = document.createElement("div");
|
||||
const i = document.createElement("i");
|
||||
|
||||
const errIndex = Math.max(...errorLevels);
|
||||
|
||||
i.className = "mdi " + icon[errIndex];
|
||||
i.title = messages.join("\n");
|
||||
|
||||
const text = document.createTextNode(
|
||||
peers + "/" + seeders + " (" + Humanize.bytes(pieceSize, 1024) + " chunks) "
|
||||
);
|
||||
|
||||
div.className = level[errIndex];
|
||||
div.appendChild(text);
|
||||
div.appendChild(i);
|
||||
|
||||
return div.outerHTML;
|
||||
});
|
||||
|
||||
tstor.routes = {
|
||||
_template: null,
|
||||
|
||||
_getTemplate: function () {
|
||||
if (this._template != null) {
|
||||
return this._template;
|
||||
}
|
||||
|
||||
const tTemplate = fetch("/assets/templates/routes.html")
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
} else {
|
||||
tstor.message.error(
|
||||
"Error getting data from server. Response: " + response.status
|
||||
);
|
||||
}
|
||||
})
|
||||
.then((t) => {
|
||||
return Handlebars.compile(t);
|
||||
})
|
||||
.catch((error) => {
|
||||
tstor.message.error("Error getting routes template: " + error.message);
|
||||
});
|
||||
|
||||
this._template = tTemplate;
|
||||
return tTemplate;
|
||||
},
|
||||
|
||||
_getRoutesJson: function () {
|
||||
return fetch("/api/routes")
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
tstor.message.error(
|
||||
"Error getting data from server. Response: " + response.status
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(function (routes) {
|
||||
return routes;
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error("Error getting status info: " + error.message);
|
||||
});
|
||||
},
|
||||
|
||||
deleteTorrent: function (route, torrentHash) {
|
||||
var url = "/api/routes/" + route + "/torrent/" + torrentHash;
|
||||
|
||||
return fetch(url, {
|
||||
method: "DELETE",
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
tstor.message.info("Torrent deleted.");
|
||||
tstor.routes.loadView();
|
||||
} else {
|
||||
response.json().then((json) => {
|
||||
tstor.message.error(
|
||||
"Error deletting torrent. Response: " + json.error
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error("Error deletting torrent: " + error.message);
|
||||
});
|
||||
},
|
||||
|
||||
loadView: function () {
|
||||
this._getTemplate().then((t) =>
|
||||
this._getRoutesJson().then((routes) => {
|
||||
document.getElementById("template_target").innerHTML = t(routes);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
$("#new-magnet").submit(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
let route = $("#route-string :selected").val();
|
||||
let magnet = $("#magnet-url").val();
|
||||
|
||||
let url = "/api/routes/" + route + "/torrent";
|
||||
let body = JSON.stringify({ magnet: magnet });
|
||||
|
||||
document.getElementById("submit_magnet_loading").style = "display:block";
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: body,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
tstor.message.info("New magnet added.");
|
||||
tstor.routes.loadView();
|
||||
} else {
|
||||
response
|
||||
.json()
|
||||
.then((json) => {
|
||||
tstor.message.error(
|
||||
"Error adding new magnet. Response: " + json.error
|
||||
);
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error("Error adding new magnet: " + response.status);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error("Error adding torrent: " + error.message);
|
||||
})
|
||||
.then(function () {
|
||||
document.getElementById("submit_magnet_loading").style = "display:none";
|
||||
});
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
Handlebars.registerHelper("to_date", function (timestamp) {
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
});
|
||||
|
||||
tstor.servers = {
|
||||
_template: null,
|
||||
|
||||
_getTemplate: function () {
|
||||
if (this._template != null) {
|
||||
return this._template;
|
||||
}
|
||||
|
||||
const tTemplate = fetch("/assets/templates/servers.html")
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.text();
|
||||
} else {
|
||||
tstor.message.error(
|
||||
"Error getting data from server. Response: " + response.status
|
||||
);
|
||||
}
|
||||
})
|
||||
.then((t) => {
|
||||
return Handlebars.compile(t);
|
||||
})
|
||||
.catch((error) => {
|
||||
tstor.message.error("Error getting servers template: " + error.message);
|
||||
});
|
||||
|
||||
this._template = tTemplate;
|
||||
return tTemplate;
|
||||
},
|
||||
|
||||
_getRoutesJson: function () {
|
||||
return fetch("/api/servers")
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
tstor.message.error(
|
||||
"Error getting data from server. Response: " + response.status
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
tstor.message.error("Error getting status info: " + error.message);
|
||||
});
|
||||
},
|
||||
|
||||
loadView: function () {
|
||||
this._getTemplate().then((t) =>
|
||||
this._getRoutesJson().then((routes) => {
|
||||
document.getElementById("template_target").innerHTML = t(routes);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
7
assets/plugins/charts/Chart.min.js
vendored
29
assets/plugins/handlebars/handlebars.min.js
vendored
1311
assets/plugins/jquery/jquery-ui-1.12.1.css
vendored
13
assets/plugins/jquery/jquery-ui-1.12.1.min.js
vendored
4
assets/plugins/jquery/jquery.min.js
vendored
|
@ -1,16 +0,0 @@
|
|||
/*! Copyright (c) 2011 Piotr Rochala (http://rocha.la)
|
||||
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
|
||||
*
|
||||
* Version: 1.3.8
|
||||
*
|
||||
*/
|
||||
(function(e){e.fn.extend({slimScroll:function(f){var a=e.extend({width:"auto",height:"250px",size:"7px",color:"#000",position:"right",distance:"1px",start:"top",opacity:.4,alwaysVisible:!1,disableFadeOut:!1,railVisible:!1,railColor:"#333",railOpacity:.2,railDraggable:!0,railClass:"slimScrollRail",barClass:"slimScrollBar",wrapperClass:"slimScrollDiv",allowPageScroll:!1,wheelStep:20,touchScrollStep:200,borderRadius:"7px",railBorderRadius:"7px"},f);this.each(function(){function v(d){if(r){d=d||window.event;
|
||||
var c=0;d.wheelDelta&&(c=-d.wheelDelta/120);d.detail&&(c=d.detail/3);e(d.target||d.srcTarget||d.srcElement).closest("."+a.wrapperClass).is(b.parent())&&n(c,!0);d.preventDefault&&!k&&d.preventDefault();k||(d.returnValue=!1)}}function n(d,g,e){k=!1;var f=b.outerHeight()-c.outerHeight();g&&(g=parseInt(c.css("top"))+d*parseInt(a.wheelStep)/100*c.outerHeight(),g=Math.min(Math.max(g,0),f),g=0<d?Math.ceil(g):Math.floor(g),c.css({top:g+"px"}));l=parseInt(c.css("top"))/(b.outerHeight()-c.outerHeight());g=
|
||||
l*(b[0].scrollHeight-b.outerHeight());e&&(g=d,d=g/b[0].scrollHeight*b.outerHeight(),d=Math.min(Math.max(d,0),f),c.css({top:d+"px"}));b.scrollTop(g);b.trigger("slimscrolling",~~g);w();p()}function x(){u=Math.max(b.outerHeight()/b[0].scrollHeight*b.outerHeight(),30);c.css({height:u+"px"});var a=u==b.outerHeight()?"none":"block";c.css({display:a})}function w(){x();clearTimeout(B);l==~~l?(k=a.allowPageScroll,C!=l&&b.trigger("slimscroll",0==~~l?"top":"bottom")):k=!1;C=l;u>=b.outerHeight()?k=!0:(c.stop(!0,
|
||||
!0).fadeIn("fast"),a.railVisible&&m.stop(!0,!0).fadeIn("fast"))}function p(){a.alwaysVisible||(B=setTimeout(function(){a.disableFadeOut&&r||y||z||(c.fadeOut("slow"),m.fadeOut("slow"))},1E3))}var r,y,z,B,A,u,l,C,k=!1,b=e(this);if(b.parent().hasClass(a.wrapperClass)){var q=b.scrollTop(),c=b.siblings("."+a.barClass),m=b.siblings("."+a.railClass);x();if(e.isPlainObject(f)){if("height"in f&&"auto"==f.height){b.parent().css("height","auto");b.css("height","auto");var h=b.parent().parent().height();b.parent().css("height",
|
||||
h);b.css("height",h)}else"height"in f&&(h=f.height,b.parent().css("height",h),b.css("height",h));if("scrollTo"in f)q=parseInt(a.scrollTo);else if("scrollBy"in f)q+=parseInt(a.scrollBy);else if("destroy"in f){c.remove();m.remove();b.unwrap();return}n(q,!1,!0)}}else if(!(e.isPlainObject(f)&&"destroy"in f)){a.height="auto"==a.height?b.parent().height():a.height;q=e("<div></div>").addClass(a.wrapperClass).css({position:"relative",overflow:"hidden",width:a.width,height:a.height});b.css({overflow:"hidden",
|
||||
width:a.width,height:a.height});var m=e("<div></div>").addClass(a.railClass).css({width:a.size,height:"100%",position:"absolute",top:0,display:a.alwaysVisible&&a.railVisible?"block":"none","border-radius":a.railBorderRadius,background:a.railColor,opacity:a.railOpacity,zIndex:90}),c=e("<div></div>").addClass(a.barClass).css({background:a.color,width:a.size,position:"absolute",top:0,opacity:a.opacity,display:a.alwaysVisible?"block":"none","border-radius":a.borderRadius,BorderRadius:a.borderRadius,MozBorderRadius:a.borderRadius,
|
||||
WebkitBorderRadius:a.borderRadius,zIndex:99}),h="right"==a.position?{right:a.distance}:{left:a.distance};m.css(h);c.css(h);b.wrap(q);b.parent().append(c);b.parent().append(m);a.railDraggable&&c.bind("mousedown",function(a){var b=e(document);z=!0;t=parseFloat(c.css("top"));pageY=a.pageY;b.bind("mousemove.slimscroll",function(a){currTop=t+a.pageY-pageY;c.css("top",currTop);n(0,c.position().top,!1)});b.bind("mouseup.slimscroll",function(a){z=!1;p();b.unbind(".slimscroll")});return!1}).bind("selectstart.slimscroll",
|
||||
function(a){a.stopPropagation();a.preventDefault();return!1});m.hover(function(){w()},function(){p()});c.hover(function(){y=!0},function(){y=!1});b.hover(function(){r=!0;w();p()},function(){r=!1;p()});b.bind("touchstart",function(a,b){a.originalEvent.touches.length&&(A=a.originalEvent.touches[0].pageY)});b.bind("touchmove",function(b){k||b.originalEvent.preventDefault();b.originalEvent.touches.length&&(n((A-b.originalEvent.touches[0].pageY)/a.touchScrollStep,!0),A=b.originalEvent.touches[0].pageY)});
|
||||
x();"bottom"===a.start?(c.css({top:b.outerHeight()-c.outerHeight()}),n(0,!0)):"top"!==a.start&&(n(e(a.start).position().top,null,!0),a.alwaysVisible||c.hide());window.addEventListener?(this.addEventListener("DOMMouseScroll",v,!1),this.addEventListener("mousewheel",v,!1)):document.attachEvent("onmousewheel",v)}});return this}});e.fn.extend({slimscroll:e.fn.slimScroll})})(jQuery);
|
1
assets/plugins/toastr/toastr.min.css
vendored
2
assets/plugins/toastr/toastr.min.js
vendored
|
@ -1,59 +0,0 @@
|
|||
{{#.}}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card card-table-border-none">
|
||||
<div
|
||||
class="card-header justify-content-between card-header-border-bottom"
|
||||
>
|
||||
<h2>Route: {{name}}</h2>
|
||||
</div>
|
||||
<div class="card-body pt-0 pb-5">
|
||||
<table
|
||||
class="table card-table table-responsive table-responsive-large"
|
||||
style="width: 100%"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30%">Name</th>
|
||||
<th style="width: 15%">
|
||||
<i class="mdi mdi-arrow-down"></i> /
|
||||
<i class="mdi mdi-arrow-up"></i>
|
||||
</th>
|
||||
<th style="width: 15%" class="d-none d-lg-table-cell">
|
||||
Peers/Seeders
|
||||
</th>
|
||||
<th style="width: 35%" class="d-none d-lg-table-cell">Status</th>
|
||||
<th style="width: 5%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#torrentStats}}
|
||||
<tr>
|
||||
<td>{{name}}</td>
|
||||
<td>
|
||||
{{ibytes downloadedBytes timePassed}} / {{ibytes uploadedBytes
|
||||
timePassed}}
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell">
|
||||
{{{torrent_info peers seeders pieceSize}}}
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell">
|
||||
{{{torrent_status pieceChunks totalPieces}}}
|
||||
</td>
|
||||
<td>
|
||||
<i
|
||||
class="mdi mdi-delete-forever"
|
||||
title="delete torrent"
|
||||
onclick='tstor.routes.deleteTorrent("{{../name}}","{{hash}}")'
|
||||
></i>
|
||||
</td>
|
||||
</tr>
|
||||
{{/torrentStats}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/.}}
|
|
@ -1,45 +0,0 @@
|
|||
{{#.}}
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-header justify-content-between align-items-center card-header-border-bottom">
|
||||
<h2>{{name}}</h2>
|
||||
</div>
|
||||
<div class="bg-white border rounded">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-lg-4 col-xl-3">
|
||||
<div class="profile-content-left pt-5 pb-3 px-3 px-xl-5">
|
||||
<div class="contact-info pt-4">
|
||||
<h5 class="text-dark mb-1">Server Info:</h5>
|
||||
<p class="text-dark font-weight-medium pt-4 mb-2">State</p>
|
||||
<p>{{state}}</p>
|
||||
<p class="text-dark font-weight-medium pt-4 mb-2">Updated At</p>
|
||||
<p>{{to_date updatedAt}}</p>
|
||||
<p class="text-dark font-weight-medium pt-4 mb-2">Seeds: {{seeds}}</p>
|
||||
<p class="text-dark font-weight-medium pt-4 mb-2">Peers: {{peers}}</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8 col-xl-9">
|
||||
<div class="profile-content-right py-5">
|
||||
<div class="tab-content px-3 px-xl-5">
|
||||
<div class="mt-5">
|
||||
<div class="form-group mb-4">
|
||||
<label for="magnetUri">Magnet URI</label>
|
||||
<input type="text" class="form-control" id="magnetUri" value="{{magnetUri}}">
|
||||
<span class="d-block mt-1">Magnet URI pointing to the actual folder content. If content
|
||||
changes, this URI will change too.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-4">
|
||||
<label for="folder">Folder</label>
|
||||
<input type="folder" class="form-control" id="folder" value="{{folder}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/.}}
|
11
embed.go
|
@ -1,11 +0,0 @@
|
|||
package tstor
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed assets
|
||||
var Assets embed.FS
|
||||
|
||||
//go:embed templates
|
||||
var Templates embed.FS
|
|
@ -1 +0,0 @@
|
|||
Gif generated using https://ezgif.com with delay=200
|
|
@ -1 +0,0 @@
|
|||
tstor.com
|
|
@ -1 +0,0 @@
|
|||
TBD
|
|
@ -1 +0,0 @@
|
|||
TBD
|
|
@ -1,95 +0,0 @@
|
|||
## Installation
|
||||
|
||||
### Using the binary
|
||||
|
||||
Get the latest release from [releases][releases-url] page or download the source code and execute `make build`.
|
||||
|
||||
Run the program: `./tstor-[VERSION]-[OS]-[ARCH]`
|
||||
|
||||
Defaults are good enough for starters, but you can change them. Here is the output of `./tstor -help`:
|
||||
|
||||
```text
|
||||
NAME:
|
||||
tstor - Torrent client with on-demand file downloading as a filesystem.
|
||||
|
||||
USAGE:
|
||||
tstor [global options] [arguments...]
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--config value YAML file containing tstor configuration. (default: "./tstor-data/config.yaml") [$tstor_CONFIG]
|
||||
--http-port value HTTP port for web interface (default: 4444) [$tstor_HTTP_PORT]
|
||||
--fuse-allow-other Allow other users to acces to all fuse mountpoints. You need to add user_allow_other flag to /etc/fuse.conf file. (default: false) [$tstor_FUSE_ALLOW_OTHER]
|
||||
--help, -h show help (default: false)
|
||||
```
|
||||
|
||||
#### Prerequisites on windows
|
||||
|
||||
Download and install [WinFsp](http://www.secfs.net/winfsp/).
|
||||
|
||||
### Using Docker
|
||||
|
||||
Docker run example:
|
||||
|
||||
```shell
|
||||
docker run \
|
||||
--rm -p 4444:4444 -p 36911:36911 \
|
||||
--cap-add SYS_ADMIN \
|
||||
--device /dev/fuse \
|
||||
--security-opt apparmor:unconfined \
|
||||
-v /tmp/mount:/tstor-data/mount:shared \
|
||||
-v /tmp/metadata:/tstor-data/metadata \
|
||||
-v /tmp/config:/tstor-data/config \
|
||||
tstor/tstor:latest
|
||||
```
|
||||
|
||||
Docker compose example:
|
||||
|
||||
```yaml
|
||||
tstor:
|
||||
container_name: tstor
|
||||
image: tstor/tstor:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "4444:4444/tcp"
|
||||
- "36911:36911/tcp"
|
||||
volumes:
|
||||
- /home/user/mount:/tstor-data/mount:shared
|
||||
- /home/user/metadata:/tstor-data/metadata
|
||||
- /home/user/config:/tstor-data/config
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
devices:
|
||||
- /dev/fuse
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After executing and load all torrent or magnet files, a web interface will be available at `http://localhost:4444`
|
||||
It contains information about the mounted routes and torrent files like download/upload speed, leechers, seeders...
|
||||
|
||||
### Configuration File
|
||||
|
||||
You can see the default configuration file with some explanation comments [here](https://git.kmsign.ru/royalcat/tstor/blob/master/templates/config_template.yaml).
|
||||
|
||||
### Routes
|
||||
|
||||
Here there is a list of all available routes with their torrents and some info. You can add and remove torrents from here too.
|
||||
|
||||
![routes screen](images/tstor-routes-border-large.png)
|
||||
|
||||
### Servers
|
||||
|
||||
Servers is a way to generate magnet files from folders.
|
||||
All servers configured using the config yaml file will be here.
|
||||
When some data is changed on these folders, a new magnet URI will be generated.
|
||||
You can share that magnet URI with anyone to share these files.
|
||||
|
||||
![server screen](images/tstor-server-border.png)
|
||||
|
||||
### Logs
|
||||
|
||||
You can check logs in real time from the web interface:
|
||||
|
||||
![logs screen](images/tstor-logs-border.png)
|
Before Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 3.7 MiB |
Before Width: | Height: | Size: 46 KiB |
|
@ -1,36 +0,0 @@
|
|||
tstor is an alternative torrent client.
|
||||
It can expose torrent files as a standard FUSE mount or webDAV endpoint and download them on demand, allowing random reads using a fixed amount of disk space.
|
||||
|
||||
![tstor Screen Shot][product-screenshot]
|
||||
|
||||
[product-screenshot]: images/tstor.gif
|
||||
|
||||
## Features
|
||||
|
||||
### User Interfaces
|
||||
|
||||
tstor supports several ways to expose the files to the user or external applications:
|
||||
|
||||
#### Supported
|
||||
|
||||
- FUSE: Other applications can access to torrent files directly as a filesystem.
|
||||
- WebDAV: Applications that supports WebDAV can access torrent files using this protocol. It is recommended when tstor is running in a remote machine or using docker.
|
||||
- HTTP: A simple HTTP interface for all the available routes. You can acces it from `http://[HOST]:[PORT]/fs`
|
||||
|
||||
### _Expandable_ File Formats
|
||||
|
||||
tstor can show some kind of files directly as folders, making it possible for applications read only the parts that they need. Here is a list of supported, to be supported and not supported formats.
|
||||
|
||||
#### Supported
|
||||
|
||||
- zip: Able to uncompress just one file. The file is decompressed to a temporal file sequentially to make possible seek over it. The decompression stops if no one is reading it.
|
||||
- rar: Thanks to [rardecode](https://github.com/nwaples/rardecode/tree/experimental) experimental branch library, it is possible to seek through rar files.
|
||||
- 7zip: Thanks to [sevenzip](https://github.com/bodgit/sevenzip) library, it is possible to read `7z` files in a similar way that is done using the `zip` implementation.
|
||||
|
||||
#### To Be Supported
|
||||
|
||||
- xz: Only worth it when the file is created using blocks. Possible library [here](https://github.com/ulikunitz/xz) and [here](https://github.com/frrad/bxzf).
|
||||
|
||||
#### Not Supported
|
||||
|
||||
- gzip: As far as I know, it doesn't support random access.
|
|
@ -1 +0,0 @@
|
|||
TBD
|
|
@ -1,45 +0,0 @@
|
|||
site_name: tstor
|
||||
site_url: https://tstor.com/
|
||||
repo_url: https://git.kmsign.ru/royalcat/tstor
|
||||
repo_name: tstor/tstor
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started: getting-started.md
|
||||
- Tutorials: tutorials.md
|
||||
- API Reference: api-reference.md
|
||||
|
||||
theme:
|
||||
logo: images/tstor_icon.png
|
||||
icon:
|
||||
repo: fontawesome/brands/github
|
||||
name: material
|
||||
palette:
|
||||
primary: white
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.sections
|
||||
- navigation.instant
|
||||
- navigation.tracking
|
||||
- navigation.expand
|
||||
- navigation.indexes
|
||||
- navigation.top
|
||||
- toc.integrate
|
||||
|
||||
edit_uri: edit/master/mkdocs/docs/
|
||||
|
||||
plugins:
|
||||
- git-revision-date
|
||||
- search
|
||||
|
||||
extra:
|
||||
version:
|
||||
default: latest
|
||||
provider: mike
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.highlight
|
||||
- pymdownx.superfences
|
|
@ -1,5 +0,0 @@
|
|||
mkdocs==1.3.0
|
||||
mkdocs-git-revision-date-plugin==0.3.1
|
||||
mkdocs-material==7.3.2
|
||||
mkdocs-material-extensions==1.0.3
|
||||
mike==1.1.2
|
|
@ -28,39 +28,10 @@ func New(fc *filecache.Cache, s *torrent.Daemon, vfs vfs.Filesystem, logPath str
|
|||
|
||||
echopprof.Register(r)
|
||||
|
||||
// r.GET("/assets/*filepath", func(c *echo.Context) {
|
||||
// c.FileFromFS(c.Request.URL.Path, http.FS(tstor.Assets))
|
||||
// })
|
||||
|
||||
// t, err := vfstemplate.ParseGlob(http.FS(tstor.Templates), nil, "/templates/*")
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("error parsing html: %w", err)
|
||||
// }
|
||||
|
||||
// r.SetHTMLTemplate(t)
|
||||
|
||||
// r.GET("/", indexHandler)
|
||||
// // r.GET("/routes", routesHandler(ss))
|
||||
// r.GET("/logs", logsHandler)
|
||||
// r.GET("/servers", serversFoldersHandler())
|
||||
r.Any("/graphql", echo.WrapHandler((GraphQLHandler(s, vfs))))
|
||||
|
||||
// api := r.Group("/api")
|
||||
// {
|
||||
// api.GET("/log", apiLogHandler(logPath))
|
||||
// api.GET("/status", apiStatusHandler(fc, ss))
|
||||
// // api.GET("/servers", apiServersHandler(tss))
|
||||
// // api.GET("/routes", apiRoutesHandler(ss))
|
||||
// // api.POST("/routes/:route/torrent", apiAddTorrentHandler(s))
|
||||
// // api.DELETE("/routes/:route/torrent/:torrent_hash", apiDelTorrentHandler(s))
|
||||
// }
|
||||
|
||||
log.Info("starting webserver", "host", fmt.Sprintf("%s:%d", cfg.WebUi.IP, cfg.WebUi.Port))
|
||||
|
||||
// if err := r.Run(fmt.Sprintf("%s:%d", cfg.WebUi.IP, cfg.WebUi.Port)); err != nil {
|
||||
// return fmt.Errorf("error initializing server: %w", err)
|
||||
// }
|
||||
|
||||
go r.Start((fmt.Sprintf("%s:%d", cfg.WebUi.IP, cfg.WebUi.Port)))
|
||||
|
||||
return nil
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<script src="assets/plugins/jquery/jquery.min.js"></script>
|
||||
<script src="assets/plugins/slimscrollbar/jquery.slimscroll.min.js"></script>
|
||||
<script src="assets/plugins/charts/Chart.min.js"></script>
|
||||
|
||||
<script src="assets/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script src="assets/plugins/toastr/toastr.min.js"></script>
|
||||
<script src="assets/plugins/handlebars/handlebars.min.js"></script>
|
||||
|
||||
<script src="assets/js/humanize.js"></script>
|
||||
<script src="assets/js/common.js"></script>
|
|
@ -1,19 +0,0 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>tstor - {{.}}</title>
|
||||
|
||||
<!-- GOOGLE FONTS -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Montserrat:400,500|Poppins:400,500,600,700|Roboto:400,500"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.materialdesignicons.com/4.4.95/css/materialdesignicons.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- SLEEK CSS -->
|
||||
<link id="sleek-css" rel="stylesheet" href="assets/css/sleek.min.css" />
|
||||
<link href="assets/plugins/toastr/toastr.min.css" rel="stylesheet" />
|
||||
<!-- FAVICON -->
|
||||
<link href="assets/img/favicon.png" rel="shortcut icon" />
|
|
@ -1,106 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
{{template "header.html" "Dashboard"}}
|
||||
</head>
|
||||
|
||||
|
||||
<body class="header-fixed sidebar-fixed sidebar-dark header-light" id="body">
|
||||
<div class="wrapper">
|
||||
{{template "navbar.html" "dashboard"}}
|
||||
|
||||
<div class="page-wrapper">
|
||||
<!-- Header -->
|
||||
<header class="main-header " id="header">
|
||||
<nav class="navbar navbar-static-top navbar-expand-lg">
|
||||
<!-- Sidebar toggle button -->
|
||||
<button id="sidebar-toggler" class="sidebar-toggle">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<div class="row">
|
||||
<div class="col-xl-12 col-md-12">
|
||||
<!-- Sales Graph -->
|
||||
<div class="card card-default" data-scroll-height="675">
|
||||
<div class="card-header">
|
||||
<h2>Download/Upload speed</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="chart-general-network" class="chartjs"></canvas>
|
||||
</div>
|
||||
<div class="card-footer d-flex flex-wrap bg-white p-0">
|
||||
<div class="col-6 px-0">
|
||||
<div class="text-center p-4">
|
||||
<h4 id="general-download-speed">...</h4>
|
||||
<p class="mt-2">Download Speed</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 px-0">
|
||||
<div class="text-center p-4 border-left">
|
||||
<h4 id="general-upload-speed">...</h4>
|
||||
<p class="mt-2">Upload Speed</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xl-4 col-md-12">
|
||||
<!-- Doughnut Chart -->
|
||||
<div class="card card-default" data-scroll-height="675">
|
||||
<div class="card-header justify-content-center">
|
||||
<h2>Main cache</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="main-cache-chart"></canvas>
|
||||
</div>
|
||||
<div class="card-footer d-flex flex-wrap bg-white p-0">
|
||||
<div class="col-6">
|
||||
<div class="py-4 px-4">
|
||||
<ul class="d-flex flex-column justify-content-between">
|
||||
<li class="mb-2"><i class="mdi mdi-checkbox-blank-circle-outline mr-2"
|
||||
style="color: #4c84ff"></i>Used</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 border-left">
|
||||
<div class="py-4 px-4 ">
|
||||
<ul class="d-flex flex-column justify-content-between">
|
||||
<li class="mb-2"><i class="mdi mdi-checkbox-blank-circle-outline mr-2"
|
||||
style="color: #8061ef"></i>Free</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer mt-auto">
|
||||
<div class="copyright bg-white">
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "footer.html"}}
|
||||
<script src="assets/js/general_chart.js"></script>
|
||||
<script src="assets/js/cache_chart.js"></script>
|
||||
<script src="assets/js/dashboard.js"></script>
|
||||
<script>
|
||||
tstor.dashboard.loadView();
|
||||
setInterval(function () {
|
||||
tstor.dashboard.loadView();
|
||||
}, 2000)
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,77 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
{{template "header.html" "Logs"}}
|
||||
</head>
|
||||
|
||||
|
||||
<body class="header-fixed sidebar-fixed sidebar-dark header-light" id="body">
|
||||
<div class="wrapper">
|
||||
{{template "navbar.html" "logs"}}
|
||||
|
||||
<div class="page-wrapper">
|
||||
<!-- Header -->
|
||||
<header class="main-header " id="header">
|
||||
<nav class="navbar navbar-static-top navbar-expand-lg">
|
||||
<!-- Sidebar toggle button -->
|
||||
<button id="sidebar-toggler" class="sidebar-toggle">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card card-default" data-scroll-height="1000"
|
||||
style="height: 1000px; overflow: hidden;">
|
||||
<div
|
||||
class="card-header justify-content-between align-items-center card-header-border-bottom">
|
||||
<h2>Logs</h2>
|
||||
</div>
|
||||
<div style="position: relative; overflow: hidden; width: auto; height: 100%;">
|
||||
<div class="card-body"
|
||||
style="overflow: hidden; width: auto; height: 100%; overflow-y: scroll; display: flex; flex-direction: column-reverse;">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 5%">Time</th>
|
||||
<th scope="col" style="width: 5%">Type</th>
|
||||
<th scope="col" style="width: 15%">Component</th>
|
||||
<th scope="col" style="width: 40%">Message</th>
|
||||
<th scope="col" style="width: 35%">Properties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="log_table">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="slimScrollBar"
|
||||
style="background: rgb(153, 153, 153) none repeat scroll 0% 0%; width: 5px; position: absolute; top: 0px; opacity: 0.4; display: none; border-radius: 7px; z-index: 99; right: 1px; height: 242.986px;">
|
||||
</div>
|
||||
<div class="slimScrollRail"
|
||||
style="width: 5px; height: 100%; position: absolute; top: 0px; display: none; border-radius: 7px; background: rgb(51, 51, 51) none repeat scroll 0% 0%; opacity: 0.2; z-index: 90; right: 1px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer mt-auto">
|
||||
<div class="copyright bg-white">
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{{template "footer.html"}}
|
||||
|
||||
<script src="assets/js/logs.js"></script>
|
||||
<script>
|
||||
tstor.logs.loadView();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,89 +0,0 @@
|
|||
<!--
|
||||
====================================
|
||||
——— LEFT SIDEBAR WITH FOOTER
|
||||
=====================================
|
||||
-->
|
||||
<aside class="left-sidebar bg-sidebar">
|
||||
<div id="sidebar" class="sidebar sidebar-with-footer">
|
||||
<!-- Aplication Brand -->
|
||||
<div class="app-brand">
|
||||
<a href="/" title="tstor">
|
||||
<img src="/assets/img/favicon.png" />
|
||||
<span class="brand-name text-truncate">tstor</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- begin sidebar scrollbar -->
|
||||
<div class="sidebar-scrollbar">
|
||||
|
||||
<!-- sidebar menu -->
|
||||
<ul class="nav sidebar-inner" id="sidebar-menu">
|
||||
{{if eq . "dashboard"}}
|
||||
<li class="active">
|
||||
{{else}}
|
||||
<li>
|
||||
{{end}}
|
||||
<a class="sidenav-item-link" href="/">
|
||||
<i class="mdi mdi-view-dashboard-outline"></i>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
{{if eq . "routes"}}
|
||||
<li class="active">
|
||||
{{else}}
|
||||
<li>
|
||||
{{end}}
|
||||
<a class="sidenav-item-link" href="/routes">
|
||||
<i class="mdi mdi-folder-multiple-outline"></i>
|
||||
<span class="nav-text">Routes</span>
|
||||
</a>
|
||||
</li>
|
||||
{{if eq . "served-folders"}}
|
||||
<li class="active">
|
||||
{{else}}
|
||||
<li>
|
||||
{{end}}
|
||||
<a class="sidenav-item-link" href="/servers">
|
||||
<i class="mdi mdi-folder-upload-outline"></i>
|
||||
<span class="nav-text">Served Folders</span>
|
||||
</a>
|
||||
</li>
|
||||
{{if eq . "logs"}}
|
||||
<li class="active">
|
||||
{{else}}
|
||||
<li>
|
||||
{{end}}
|
||||
<a class="sidenav-item-link" href="/logs">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
<span class="nav-text">Logs</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="sidenav-item-link" href="/fs" target="_blank">
|
||||
<i class="mdi mdi-application-export"></i>
|
||||
<span class="nav-text">HTTPFS</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <div class="sidebar-footer">
|
||||
<hr class="separator mb-0" />
|
||||
<div class="sidebar-footer-content">
|
||||
<h6 class="text-uppercase">
|
||||
Main cache <span class="float-right">40%</span>
|
||||
</h6>
|
||||
<div class="progress progress-xs">
|
||||
<div class="progress-bar active" style="width: 40%;" role="progressbar"></div>
|
||||
</div>
|
||||
<h6 class="text-uppercase">
|
||||
/path/pepe cache <span class="float-right">65%</span>
|
||||
</h6>
|
||||
<div class="progress progress-xs">
|
||||
<div class="progress-bar active" style="width: 65%;" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</aside>
|
|
@ -1,80 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
{{template "header.html" "Routes"}}
|
||||
</head>
|
||||
|
||||
|
||||
<body class="header-fixed sidebar-fixed sidebar-dark header-light" id="body">
|
||||
<div class="wrapper">
|
||||
{{template "navbar.html" "routes"}}
|
||||
|
||||
<div class="page-wrapper">
|
||||
<!-- Header -->
|
||||
<header class="main-header " id="header">
|
||||
<nav class="navbar navbar-static-top navbar-expand-lg">
|
||||
<!-- Sidebar toggle button -->
|
||||
<button id="sidebar-toggler" class="sidebar-toggle">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content">
|
||||
<div id="template_target"></div>
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-header card-header-border-bottom">
|
||||
<h2>Add New Magnet to a Route</h2>
|
||||
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="new-magnet">
|
||||
<div class="form-group">
|
||||
<label>Magnet Link</label>
|
||||
<input type="text" id="magnet-url" class="form-control"
|
||||
placeholder="Enter Valid Magnet Link">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Route</label>
|
||||
<select class="form-control" id="route-string">
|
||||
{{range .}}
|
||||
<option value="{{.Name}}">{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary btn-default">Add</button>
|
||||
</div>
|
||||
<div class="form-footer pt-4 pt-5 mt-4 border-top">
|
||||
<div class="sk-double-bounce" id="submit_magnet_loading" style="display: none;">
|
||||
<div class="double-bounce1"></div>
|
||||
<div class="double-bounce2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer mt-auto">
|
||||
<div class="copyright bg-white">
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{{template "footer.html"}}
|
||||
|
||||
<script src="assets/js/routes.js"></script>
|
||||
<script>
|
||||
tstor.routes.loadView();
|
||||
setInterval(function () {
|
||||
tstor.routes.loadView();
|
||||
}, 2000)
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,47 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
{{template "header.html" "Served Folders"}}
|
||||
</head>
|
||||
|
||||
|
||||
<body class="header-fixed sidebar-fixed sidebar-dark header-light" id="body">
|
||||
<div class="wrapper">
|
||||
{{template "navbar.html" "served-folders"}}
|
||||
|
||||
<div class="page-wrapper">
|
||||
<!-- Header -->
|
||||
<header class="main-header " id="header">
|
||||
<nav class="navbar navbar-static-top navbar-expand-lg">
|
||||
<!-- Sidebar toggle button -->
|
||||
<button id="sidebar-toggler" class="sidebar-toggle">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content" id="template_target">
|
||||
|
||||
</div>
|
||||
<footer class="footer mt-auto">
|
||||
<div class="copyright bg-white">
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{{template "footer.html"}}
|
||||
|
||||
<script src="assets/js/servers.js"></script>
|
||||
<script>
|
||||
tstor.servers.loadView();
|
||||
setInterval(function () {
|
||||
tstor.servers.loadView();
|
||||
}, 5000)
|
||||
|
||||
</script>
|
||||
<script>
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
1
tools.go
|
@ -1,7 +1,6 @@
|
|||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
//go:generate go run github.com/99designs/gqlgen
|
||||
package tstor
|
||||
|
||||
import (
|
||||
|
|