Add and remove torrents from web interface (#84)
This commit is contained in:
parent
02842b1917
commit
2f18213660
49 changed files with 996 additions and 1170 deletions
|
@ -77,7 +77,6 @@ Handlebars.registerHelper("torrent_info", function (peers, seeders, pieceSize) {
|
|||
return div.outerHTML;
|
||||
});
|
||||
|
||||
|
||||
Distribyted.routes = {
|
||||
_template: null,
|
||||
|
||||
|
@ -114,45 +113,6 @@ Distribyted.routes = {
|
|||
Distribyted.message.error('Error getting data from server. Response: ' + response.status)
|
||||
}
|
||||
}).then(function (routes) {
|
||||
// routes.forEach(route => {
|
||||
// route.torrentStats.forEach(torrentStat => {
|
||||
// fileChunks.update(torrentStat.pieceChunks, torrentStat.totalPieces, torrentStat.hash);
|
||||
|
||||
// var download = torrentStat.downloadedBytes / torrentStat.timePassed;
|
||||
// var upload = torrentStat.uploadedBytes / torrentStat.timePassed;
|
||||
// var seeders = torrentStat.seeders;
|
||||
// var peers = torrentStat.peers;
|
||||
// var pieceSize = torrentStat.pieceSize;
|
||||
|
||||
// document.getElementById("up-down-speed-text-" + torrentStat.hash).innerText =
|
||||
// Humanize.ibytes(download, 1024) + "/s down, " + Humanize.ibytes(upload, 1024) + "/s up";
|
||||
// document.getElementById("peers-seeders-" + torrentStat.hash).innerText =
|
||||
// peers + " peers, " + seeders + " seeders."
|
||||
// document.getElementById("piece-size-" + torrentStat.hash).innerText = "Piece size: " + Humanize.bytes(pieceSize, 1024)
|
||||
|
||||
// var className = "";
|
||||
|
||||
// if (seeders < 2) {
|
||||
// className = "text-danger";
|
||||
// } else if (seeders >= 2 && seeders < 4) {
|
||||
// className = "text-warning";
|
||||
// } else {
|
||||
// className = "text-success";
|
||||
// }
|
||||
|
||||
// document.getElementById("peers-seeders-" + torrentStat.hash).className = className;
|
||||
|
||||
// if (pieceSize <= MB) {
|
||||
// className = "text-success";
|
||||
// } else if (pieceSize > MB && pieceSize < (MB * 4)) {
|
||||
// className = "text-warning";
|
||||
// } else {
|
||||
// className = "text-danger";
|
||||
// }
|
||||
|
||||
// document.getElementById("piece-size-" + torrentStat.hash).className = className;
|
||||
// });
|
||||
// });
|
||||
return routes;
|
||||
})
|
||||
.catch(function (error) {
|
||||
|
@ -160,6 +120,26 @@ Distribyted.routes = {
|
|||
});
|
||||
},
|
||||
|
||||
deleteTorrent: function (route, torrentHash) {
|
||||
var url = '/api/routes/' + route + '/torrent/' + torrentHash
|
||||
|
||||
return fetch(url, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
Distribyted.message.info('Torrent deleted.')
|
||||
} else {
|
||||
response.json().then(json => {
|
||||
Distribyted.message.error('Error deletting torrent. Response: ' + json.error)
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
Distribyted.message.error('Error deletting torrent: ' + error.message)
|
||||
});
|
||||
},
|
||||
|
||||
loadView: function () {
|
||||
this._getTemplate()
|
||||
.then(t =>
|
||||
|
@ -169,3 +149,33 @@ Distribyted.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 })
|
||||
|
||||
console.log("LOG", url, body)
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: body
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
Distribyted.message.info('New magnet added.')
|
||||
} else {
|
||||
response.json().then(json => {
|
||||
Distribyted.message.error('Error adding new magnet. Response: ' + json.error)
|
||||
}).catch(function (error) {
|
||||
Distribyted.message.error('Error adding new magnet: ' + response.status)
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
Distribyted.message.error('Error deletting torrent: ' + error.message)
|
||||
});
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/elastic_tabstops_lite",["require","exports","module","ace/editor","ace/config"],function(e,t,n){"use strict";var r=function(e){this.$editor=e;var t=this,n=[],r=!1;this.onAfterExec=function(){r=!1,t.processRows(n),n=[]},this.onExec=function(){r=!0},this.onChange=function(e){r&&(n.indexOf(e.start.row)==-1&&n.push(e.start.row),e.end.row!=e.start.row&&n.push(e.end.row))}};(function(){this.processRows=function(e){this.$inChange=!0;var t=[];for(var n=0,r=e.length;n<r;n++){var i=e[n];if(t.indexOf(i)>-1)continue;var s=this.$findCellWidthsForBlock(i),o=this.$setBlockCellWidthsToMax(s.cellWidths),u=s.firstRow;for(var a=0,f=o.length;a<f;a++){var l=o[a];t.push(u),this.$adjustRow(u,l),u++}}this.$inChange=!1},this.$findCellWidthsForBlock=function(e){var t=[],n,r=e;while(r>=0){n=this.$cellWidthsForRow(r);if(n.length==0)break;t.unshift(n),r--}var i=r+1;r=e;var s=this.$editor.session.getLength();while(r<s-1){r++,n=this.$cellWidthsForRow(r);if(n.length==0)break;t.push(n)}return{cellWidths:t,firstRow:i}},this.$cellWidthsForRow=function(e){var t=this.$selectionColumnsForRow(e),n=[-1].concat(this.$tabsForRow(e)),r=n.map(function(e){return 0}).slice(1),i=this.$editor.session.getLine(e);for(var s=0,o=n.length-1;s<o;s++){var u=n[s]+1,a=n[s+1],f=this.$rightmostSelectionInCell(t,a),l=i.substring(u,a);r[s]=Math.max(l.replace(/\s+$/g,"").length,f-u)}return r},this.$selectionColumnsForRow=function(e){var t=[],n=this.$editor.getCursorPosition();return this.$editor.session.getSelection().isEmpty()&&e==n.row&&t.push(n.column),t},this.$setBlockCellWidthsToMax=function(e){var t=!0,n,r,i,s=this.$izip_longest(e);for(var o=0,u=s.length;o<u;o++){var a=s[o];if(!a.push){console.error(a);continue}a.push(NaN);for(var f=0,l=a.length;f<l;f++){var c=a[f];t&&(n=f,i=0,t=!1);if(isNaN(c)){r=f;for(var h=n;h<r;h++)e[h][o]=i;t=!0}i=Math.max(i,c)}}return e},this.$rightmostSelectionInCell=function(e,t){var n=0;if(e.length){var r=[];for(var i=0,s=e.length;i<s;i++)e[i]<=t?r.push(i):r.push(0);n=Math.max.apply(Math,r)}return n},this.$tabsForRow=function(e){var t=[],n=this.$editor.session.getLine(e),r=/\t/g,i;while((i=r.exec(n))!=null)t.push(i.index);return t},this.$adjustRow=function(e,t){var n=this.$tabsForRow(e);if(n.length==0)return;var r=0,i=-1,s=this.$izip(t,n);for(var o=0,u=s.length;o<u;o++){var a=s[o][0],f=s[o][1];i+=1+a,f+=r;var l=i-f;if(l==0)continue;var c=this.$editor.session.getLine(e).substr(0,f),h=c.replace(/\s*$/g,""),p=c.length-h.length;l>0&&(this.$editor.session.getDocument().insertInLine({row:e,column:f+1},Array(l+1).join(" ")+" "),this.$editor.session.getDocument().removeInLine(e,f,f+1),r+=l),l<0&&p>=-l&&(this.$editor.session.getDocument().removeInLine(e,f+l,f),r+=l)}},this.$izip_longest=function(e){if(!e[0])return[];var t=e[0].length,n=e.length;for(var r=1;r<n;r++){var i=e[r].length;i>t&&(t=i)}var s=[];for(var o=0;o<t;o++){var u=[];for(var r=0;r<n;r++)e[r][o]===""?u.push(NaN):u.push(e[r][o]);s.push(u)}return s},this.$izip=function(e,t){var n=e.length>=t.length?t.length:e.length,r=[];for(var i=0;i<n;i++){var s=[e[i],t[i]];r.push(s)}return r}}).call(r.prototype),t.ElasticTabstopsLite=r;var i=e("../editor").Editor;e("../config").defineOptions(i.prototype,"editor",{useElasticTabstops:{set:function(e){e?(this.elasticTabstops||(this.elasticTabstops=new r(this)),this.commands.on("afterExec",this.elasticTabstops.onAfterExec),this.commands.on("exec",this.elasticTabstops.onExec),this.on("change",this.elasticTabstops.onChange)):this.elasticTabstops&&(this.commands.removeListener("afterExec",this.elasticTabstops.onAfterExec),this.commands.removeListener("exec",this.elasticTabstops.onExec),this.removeListener("change",this.elasticTabstops.onChange))}}})}); (function() {
|
||||
ace.require(["ace/ext/elastic_tabstops_lite"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
; (function() {
|
||||
ace.require(["ace/ext/error_marker"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/menu_tools/overlay_page",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../../lib/dom"),i="#ace_settingsmenu, #kbshortcutmenu {background-color: #F7F7F7;color: black;box-shadow: -5px 4px 5px rgba(126, 126, 126, 0.55);padding: 1em 0.5em 2em 1em;overflow: auto;position: absolute;margin: 0;bottom: 0;right: 0;top: 0;z-index: 9991;cursor: default;}.ace_dark #ace_settingsmenu, .ace_dark #kbshortcutmenu {box-shadow: -20px 10px 25px rgba(126, 126, 126, 0.25);background-color: rgba(255, 255, 255, 0.6);color: black;}.ace_optionsMenuEntry:hover {background-color: rgba(100, 100, 100, 0.1);transition: all 0.3s}.ace_closeButton {background: rgba(245, 146, 146, 0.5);border: 1px solid #F48A8A;border-radius: 50%;padding: 7px;position: absolute;right: -8px;top: -8px;z-index: 100000;}.ace_closeButton{background: rgba(245, 146, 146, 0.9);}.ace_optionsMenuKey {color: darkslateblue;font-weight: bold;}.ace_optionsMenuCommand {color: darkcyan;font-weight: normal;}.ace_optionsMenuEntry input, .ace_optionsMenuEntry button {vertical-align: middle;}.ace_optionsMenuEntry button[ace_selected_button=true] {background: #e7e7e7;box-shadow: 1px 0px 2px 0px #adadad inset;border-color: #adadad;}.ace_optionsMenuEntry button {background: white;border: 1px solid lightgray;margin: 0px;}.ace_optionsMenuEntry button:hover{background: #f0f0f0;}";r.importCssString(i),n.exports.overlayPage=function(t,n,r){function o(e){e.keyCode===27&&u()}function u(){if(!i)return;document.removeEventListener("keydown",o),i.parentNode.removeChild(i),t&&t.focus(),i=null,r&&r()}function a(e){s=e,e&&(i.style.pointerEvents="none",n.style.pointerEvents="auto")}var i=document.createElement("div"),s=!1;return i.style.cssText="margin: 0; padding: 0; position: fixed; top:0; bottom:0; left:0; right:0;z-index: 9990; "+(t?"background-color: rgba(0, 0, 0, 0.3);":""),i.addEventListener("click",function(e){s||u()}),document.addEventListener("keydown",o),n.addEventListener("click",function(e){e.stopPropagation()}),i.appendChild(n),document.body.appendChild(i),t&&t.blur(),{close:u,setIgnoreFocusOut:a}}}),ace.define("ace/ext/menu_tools/get_editor_keyboard_shortcuts",["require","exports","module","ace/lib/keys"],function(e,t,n){"use strict";var r=e("../../lib/keys");n.exports.getEditorKeybordShortcuts=function(e){var t=r.KEY_MODS,n=[],i={};return e.keyBinding.$handlers.forEach(function(e){var t=e.commandKeyBinding;for(var r in t){var s=r.replace(/(^|-)\w/g,function(e){return e.toUpperCase()}),o=t[r];Array.isArray(o)||(o=[o]),o.forEach(function(e){typeof e!="string"&&(e=e.name),i[e]?i[e].key+="|"+s:(i[e]={key:s,command:e},n.push(i[e]))})}}),n}}),ace.define("ace/ext/keybinding_menu",["require","exports","module","ace/editor","ace/ext/menu_tools/overlay_page","ace/ext/menu_tools/get_editor_keyboard_shortcuts"],function(e,t,n){"use strict";function i(t){if(!document.getElementById("kbshortcutmenu")){var n=e("./menu_tools/overlay_page").overlayPage,r=e("./menu_tools/get_editor_keyboard_shortcuts").getEditorKeybordShortcuts,i=r(t),s=document.createElement("div"),o=i.reduce(function(e,t){return e+'<div class="ace_optionsMenuEntry"><span class="ace_optionsMenuCommand">'+t.command+"</span> : "+'<span class="ace_optionsMenuKey">'+t.key+"</span></div>"},"");s.id="kbshortcutmenu",s.innerHTML="<h1>Keyboard Shortcuts</h1>"+o+"</div>",n(t,s)}}var r=e("../editor").Editor;n.exports.init=function(e){r.prototype.showKeyboardShortcuts=function(){i(this)},e.commands.addCommands([{name:"showKeyboardShortcuts",bindKey:{win:"Ctrl-Alt-h",mac:"Command-Alt-h"},exec:function(e,t){e.showKeyboardShortcuts()}}])}}); (function() {
|
||||
ace.require(["ace/ext/keybinding_menu"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/linking",["require","exports","module","ace/editor","ace/config"],function(e,t,n){function i(e){var n=e.editor,r=e.getAccelKey();if(r){var n=e.editor,i=e.getDocumentPosition(),s=n.session,o=s.getTokenAt(i.row,i.column);t.previousLinkingHover&&t.previousLinkingHover!=o&&n._emit("linkHoverOut"),n._emit("linkHover",{position:i,token:o}),t.previousLinkingHover=o}else t.previousLinkingHover&&(n._emit("linkHoverOut"),t.previousLinkingHover=!1)}function s(e){var t=e.getAccelKey(),n=e.getButton();if(n==0&&t){var r=e.editor,i=e.getDocumentPosition(),s=r.session,o=s.getTokenAt(i.row,i.column);r._emit("linkClick",{position:i,token:o})}}var r=e("../editor").Editor;e("../config").defineOptions(r.prototype,"editor",{enableLinking:{set:function(e){e?(this.on("click",s),this.on("mousemove",i)):(this.off("click",s),this.off("mousemove",i))},value:!1}}),t.previousLinkingHover=!1}); (function() {
|
||||
ace.require(["ace/ext/linking"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/modelist",["require","exports","module"],function(e,t,n){"use strict";function i(e){var t=a.text,n=e.split(/[\/\\]/).pop();for(var i=0;i<r.length;i++)if(r[i].supportsFile(n)){t=r[i];break}return t}var r=[],s=function(e,t,n){this.name=e,this.caption=t,this.mode="ace/mode/"+e,this.extensions=n;var r;/\^/.test(n)?r=n.replace(/\|(\^)?/g,function(e,t){return"$|"+(t?"^":"^.*\\.")})+"$":r="^.*\\.("+n+")$",this.extRe=new RegExp(r,"gi")};s.prototype.supportsFile=function(e){return e.match(this.extRe)};var o={ABAP:["abap"],ABC:["abc"],ActionScript:["as"],ADA:["ada|adb"],Alda:["alda"],Apache_Conf:["^htaccess|^htgroups|^htpasswd|^conf|htaccess|htgroups|htpasswd"],Apex:["apex|cls|trigger|tgr"],AQL:["aql"],AsciiDoc:["asciidoc|adoc"],ASL:["dsl|asl"],Assembly_x86:["asm|a"],AutoHotKey:["ahk"],BatchFile:["bat|cmd"],C_Cpp:["cpp|c|cc|cxx|h|hh|hpp|ino"],C9Search:["c9search_results"],Cirru:["cirru|cr"],Clojure:["clj|cljs"],Cobol:["CBL|COB"],coffee:["coffee|cf|cson|^Cakefile"],ColdFusion:["cfm"],Crystal:["cr"],CSharp:["cs"],Csound_Document:["csd"],Csound_Orchestra:["orc"],Csound_Score:["sco"],CSS:["css"],Curly:["curly"],D:["d|di"],Dart:["dart"],Diff:["diff|patch"],Dockerfile:["^Dockerfile"],Dot:["dot"],Drools:["drl"],Edifact:["edi"],Eiffel:["e|ge"],EJS:["ejs"],Elixir:["ex|exs"],Elm:["elm"],Erlang:["erl|hrl"],Forth:["frt|fs|ldr|fth|4th"],Fortran:["f|f90"],FSharp:["fsi|fs|ml|mli|fsx|fsscript"],FSL:["fsl"],FTL:["ftl"],Gcode:["gcode"],Gherkin:["feature"],Gitignore:["^.gitignore"],Glsl:["glsl|frag|vert"],Gobstones:["gbs"],golang:["go"],GraphQLSchema:["gql"],Groovy:["groovy"],HAML:["haml"],Handlebars:["hbs|handlebars|tpl|mustache"],Haskell:["hs"],Haskell_Cabal:["cabal"],haXe:["hx"],Hjson:["hjson"],HTML:["html|htm|xhtml|vue|we|wpy"],HTML_Elixir:["eex|html.eex"],HTML_Ruby:["erb|rhtml|html.erb"],INI:["ini|conf|cfg|prefs"],Io:["io"],Jack:["jack"],Jade:["jade|pug"],Java:["java"],JavaScript:["js|jsm|jsx"],JSON:["json"],JSON5:["json5"],JSONiq:["jq"],JSP:["jsp"],JSSM:["jssm|jssm_state"],JSX:["jsx"],Julia:["jl"],Kotlin:["kt|kts"],LaTeX:["tex|latex|ltx|bib"],LESS:["less"],Liquid:["liquid"],Lisp:["lisp"],LiveScript:["ls"],LogiQL:["logic|lql"],LSL:["lsl"],Lua:["lua"],LuaPage:["lp"],Lucene:["lucene"],Makefile:["^Makefile|^GNUmakefile|^makefile|^OCamlMakefile|make"],Markdown:["md|markdown"],Mask:["mask"],MATLAB:["matlab"],Maze:["mz"],MediaWiki:["wiki|mediawiki"],MEL:["mel"],MIXAL:["mixal"],MUSHCode:["mc|mush"],MySQL:["mysql"],Nginx:["nginx|conf"],Nim:["nim"],Nix:["nix"],NSIS:["nsi|nsh"],Nunjucks:["nunjucks|nunjs|nj|njk"],ObjectiveC:["m|mm"],OCaml:["ml|mli"],Pascal:["pas|p"],Perl:["pl|pm"],Perl6:["p6|pl6|pm6"],pgSQL:["pgsql"],PHP:["php|inc|phtml|shtml|php3|php4|php5|phps|phpt|aw|ctp|module"],PHP_Laravel_blade:["blade.php"],Pig:["pig"],Powershell:["ps1"],Praat:["praat|praatscript|psc|proc"],Prisma:["prisma"],Prolog:["plg|prolog"],Properties:["properties"],Protobuf:["proto"],Puppet:["epp|pp"],Python:["py"],QML:["qml"],R:["r"],Razor:["cshtml|asp"],RDoc:["Rd"],Red:["red|reds"],RHTML:["Rhtml"],RST:["rst"],Ruby:["rb|ru|gemspec|rake|^Guardfile|^Rakefile|^Gemfile"],Rust:["rs"],SASS:["sass"],SCAD:["scad"],Scala:["scala|sbt"],Scheme:["scm|sm|rkt|oak|scheme"],SCSS:["scss"],SH:["sh|bash|^.bashrc"],SJS:["sjs"],Slim:["slim|skim"],Smarty:["smarty|tpl"],snippets:["snippets"],Soy_Template:["soy"],Space:["space"],SQL:["sql"],SQLServer:["sqlserver"],Stylus:["styl|stylus"],SVG:["svg"],Swift:["swift"],Tcl:["tcl"],Terraform:["tf","tfvars","terragrunt"],Tex:["tex"],Text:["txt"],Textile:["textile"],Toml:["toml"],TSX:["tsx"],Twig:["latte|twig|swig"],Typescript:["ts|typescript|str"],Vala:["vala"],VBScript:["vbs|vb"],Velocity:["vm"],Verilog:["v|vh|sv|svh"],VHDL:["vhd|vhdl"],Visualforce:["vfp|component|page"],Wollok:["wlk|wpgm|wtest"],XML:["xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl|xaml"],XQuery:["xq"],YAML:["yaml|yml"],Zeek:["zeek|bro"],Django:["html"]},u={ObjectiveC:"Objective-C",CSharp:"C#",golang:"Go",C_Cpp:"C and C++",Csound_Document:"Csound Document",Csound_Orchestra:"Csound",Csound_Score:"Csound Score",coffee:"CoffeeScript",HTML_Ruby:"HTML (Ruby)",HTML_Elixir:"HTML (Elixir)",FTL:"FreeMarker",PHP_Laravel_blade:"PHP (Blade Template)",Perl6:"Perl 6",AutoHotKey:"AutoHotkey / AutoIt"},a={};for(var f in o){var l=o[f],c=(u[f]||f).replace(/_/g," "),h=f.toLowerCase(),p=new s(h,c,l[0]);a[h]=p,r.push(p)}n.exports={getModeForPath:i,modes:r,modesByName:a}}); (function() {
|
||||
ace.require(["ace/ext/modelist"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/spellcheck",["require","exports","module","ace/lib/event","ace/editor","ace/config"],function(e,t,n){"use strict";var r=e("../lib/event");t.contextMenuHandler=function(e){var t=e.target,n=t.textInput.getElement();if(!t.selection.isEmpty())return;var i=t.getCursorPosition(),s=t.session.getWordRange(i.row,i.column),o=t.session.getTextRange(s);t.session.tokenRe.lastIndex=0;if(!t.session.tokenRe.test(o))return;var u="\x01\x01",a=o+" "+u;n.value=a,n.setSelectionRange(o.length,o.length+1),n.setSelectionRange(0,0),n.setSelectionRange(0,o.length);var f=!1;r.addListener(n,"keydown",function l(){r.removeListener(n,"keydown",l),f=!0}),t.textInput.setInputHandler(function(e){if(e==a)return"";if(e.lastIndexOf(a,0)===0)return e.slice(a.length);if(e.substr(n.selectionEnd)==a)return e.slice(0,-a.length);if(e.slice(-2)==u){var r=e.slice(0,-2);if(r.slice(-1)==" ")return f?r.substring(0,n.selectionEnd):(r=r.slice(0,-1),t.session.replace(s,r),"")}return e})};var i=e("../editor").Editor;e("../config").defineOptions(i.prototype,"editor",{spellcheck:{set:function(e){var n=this.textInput.getElement();n.spellcheck=!!e,e?this.on("nativecontextmenu",t.contextMenuHandler):this.removeListener("nativecontextmenu",t.contextMenuHandler)},value:!0}})}); (function() {
|
||||
ace.require(["ace/ext/spellcheck"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/split",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter","ace/editor","ace/virtual_renderer","ace/edit_session"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/lang"),s=e("./lib/event_emitter").EventEmitter,o=e("./editor").Editor,u=e("./virtual_renderer").VirtualRenderer,a=e("./edit_session").EditSession,f=function(e,t,n){this.BELOW=1,this.BESIDE=0,this.$container=e,this.$theme=t,this.$splits=0,this.$editorCSS="",this.$editors=[],this.$orientation=this.BESIDE,this.setSplits(n||1),this.$cEditor=this.$editors[0],this.on("focus",function(e){this.$cEditor=e}.bind(this))};(function(){r.implement(this,s),this.$createEditor=function(){var e=document.createElement("div");e.className=this.$editorCSS,e.style.cssText="position: absolute; top:0px; bottom:0px",this.$container.appendChild(e);var t=new o(new u(e,this.$theme));return t.on("focus",function(){this._emit("focus",t)}.bind(this)),this.$editors.push(t),t.setFontSize(this.$fontSize),t},this.setSplits=function(e){var t;if(e<1)throw"The number of splits have to be > 0!";if(e==this.$splits)return;if(e>this.$splits){while(this.$splits<this.$editors.length&&this.$splits<e)t=this.$editors[this.$splits],this.$container.appendChild(t.container),t.setFontSize(this.$fontSize),this.$splits++;while(this.$splits<e)this.$createEditor(),this.$splits++}else while(this.$splits>e)t=this.$editors[this.$splits-1],this.$container.removeChild(t.container),this.$splits--;this.resize()},this.getSplits=function(){return this.$splits},this.getEditor=function(e){return this.$editors[e]},this.getCurrentEditor=function(){return this.$cEditor},this.focus=function(){this.$cEditor.focus()},this.blur=function(){this.$cEditor.blur()},this.setTheme=function(e){this.$editors.forEach(function(t){t.setTheme(e)})},this.setKeyboardHandler=function(e){this.$editors.forEach(function(t){t.setKeyboardHandler(e)})},this.forEach=function(e,t){this.$editors.forEach(e,t)},this.$fontSize="",this.setFontSize=function(e){this.$fontSize=e,this.forEach(function(t){t.setFontSize(e)})},this.$cloneSession=function(e){var t=new a(e.getDocument(),e.getMode()),n=e.getUndoManager();return t.setUndoManager(n),t.setTabSize(e.getTabSize()),t.setUseSoftTabs(e.getUseSoftTabs()),t.setOverwrite(e.getOverwrite()),t.setBreakpoints(e.getBreakpoints()),t.setUseWrapMode(e.getUseWrapMode()),t.setUseWorker(e.getUseWorker()),t.setWrapLimitRange(e.$wrapLimitRange.min,e.$wrapLimitRange.max),t.$foldData=e.$cloneFoldData(),t},this.setSession=function(e,t){var n;t==null?n=this.$cEditor:n=this.$editors[t];var r=this.$editors.some(function(t){return t.session===e});return r&&(e=this.$cloneSession(e)),n.setSession(e),e},this.getOrientation=function(){return this.$orientation},this.setOrientation=function(e){if(this.$orientation==e)return;this.$orientation=e,this.resize()},this.resize=function(){var e=this.$container.clientWidth,t=this.$container.clientHeight,n;if(this.$orientation==this.BESIDE){var r=e/this.$splits;for(var i=0;i<this.$splits;i++)n=this.$editors[i],n.container.style.width=r+"px",n.container.style.top="0px",n.container.style.left=i*r+"px",n.container.style.height=t+"px",n.resize()}else{var s=t/this.$splits;for(var i=0;i<this.$splits;i++)n=this.$editors[i],n.container.style.width=e+"px",n.container.style.top=i*s+"px",n.container.style.left="0px",n.container.style.height=s+"px",n.resize()}}}).call(f.prototype),t.Split=f}),ace.define("ace/ext/split",["require","exports","module","ace/split"],function(e,t,n){"use strict";n.exports=e("../split")}); (function() {
|
||||
ace.require(["ace/ext/split"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/static_highlight",["require","exports","module","ace/edit_session","ace/layer/text","ace/config","ace/lib/dom","ace/lib/lang"],function(e,t,n){"use strict";function f(e){this.type=e,this.style={},this.textContent=""}var r=e("../edit_session").EditSession,i=e("../layer/text").Text,s=".ace_static_highlight {font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'Droid Sans Mono', monospace;font-size: 12px;white-space: pre-wrap}.ace_static_highlight .ace_gutter {width: 2em;text-align: right;padding: 0 3px 0 0;margin-right: 3px;contain: none;}.ace_static_highlight.ace_show_gutter .ace_line {padding-left: 2.6em;}.ace_static_highlight .ace_line { position: relative; }.ace_static_highlight .ace_gutter-cell {-moz-user-select: -moz-none;-khtml-user-select: none;-webkit-user-select: none;user-select: none;top: 0;bottom: 0;left: 0;position: absolute;}.ace_static_highlight .ace_gutter-cell:before {content: counter(ace_line, decimal);counter-increment: ace_line;}.ace_static_highlight {counter-reset: ace_line;}",o=e("../config"),u=e("../lib/dom"),a=e("../lib/lang").escapeHTML;f.prototype.cloneNode=function(){return this},f.prototype.appendChild=function(e){this.textContent+=e.toString()},f.prototype.toString=function(){var e=[];if(this.type!="fragment"){e.push("<",this.type),this.className&&e.push(" class='",this.className,"'");var t=[];for(var n in this.style)t.push(n,":",this.style[n]);t.length&&e.push(" style='",t.join(""),"'"),e.push(">")}return this.textContent&&e.push(this.textContent),this.type!="fragment"&&e.push("</",this.type,">"),e.join("")};var l={createTextNode:function(e,t){return a(e)},createElement:function(e){return new f(e)},createFragment:function(){return new f("fragment")}},c=function(){this.config={},this.dom=l};c.prototype=i.prototype;var h=function(e,t,n){var r=e.className.match(/lang-(\w+)/),i=t.mode||r&&"ace/mode/"+r[1];if(!i)return!1;var s=t.theme||"ace/theme/textmate",o="",a=[];if(e.firstElementChild){var f=0;for(var l=0;l<e.childNodes.length;l++){var c=e.childNodes[l];c.nodeType==3?(f+=c.data.length,o+=c.data):a.push(f,c)}}else o=e.textContent,t.trim&&(o=o.trim());h.render(o,i,s,t.firstLineNumber,!t.showGutter,function(t){u.importCssString(t.css,"ace_highlight"),e.innerHTML=t.html;var r=e.firstChild.firstChild;for(var i=0;i<a.length;i+=2){var s=t.session.doc.indexToPosition(a[i]),o=a[i+1],f=r.children[s.row];f&&f.appendChild(o)}n&&n()})};h.render=function(e,t,n,i,s,u){function c(){var r=h.renderSync(e,t,n,i,s);return u?u(r):r}var a=1,f=r.prototype.$modes;typeof n=="string"&&(a++,o.loadModule(["theme",n],function(e){n=e,--a||c()}));var l;return t&&typeof t=="object"&&!t.getTokenizer&&(l=t,t=l.path),typeof t=="string"&&(a++,o.loadModule(["mode",t],function(e){if(!f[t]||l)f[t]=new e.Mode(l);t=f[t],--a||c()})),--a||c()},h.renderSync=function(e,t,n,i,o){i=parseInt(i||1,10);var u=new r("");u.setUseWorker(!1),u.setMode(t);var a=new c;a.setSession(u),Object.keys(a.$tabStrings).forEach(function(e){if(typeof a.$tabStrings[e]=="string"){var t=l.createFragment();t.textContent=a.$tabStrings[e],a.$tabStrings[e]=t}}),u.setValue(e);var f=u.getLength(),h=l.createElement("div");h.className=n.cssClass;var p=l.createElement("div");p.className="ace_static_highlight"+(o?"":" ace_show_gutter"),p.style["counter-reset"]="ace_line "+(i-1);for(var d=0;d<f;d++){var v=l.createElement("div");v.className="ace_line";if(!o){var m=l.createElement("span");m.className="ace_gutter ace_gutter-cell",m.textContent="",v.appendChild(m)}a.$renderLine(v,d,!1),v.textContent+="\n",p.appendChild(v)}return h.appendChild(p),{css:s+n.cssText,html:h.toString(),session:u}},n.exports=h,n.exports.highlight=h}); (function() {
|
||||
ace.require(["ace/ext/static_highlight"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/statusbar",["require","exports","module","ace/lib/dom","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=e("../lib/lang"),s=function(e,t){this.element=r.createElement("div"),this.element.className="ace_status-indicator",this.element.style.cssText="display: inline-block;",t.appendChild(this.element);var n=i.delayedCall(function(){this.updateStatus(e)}.bind(this)).schedule.bind(null,100);e.on("changeStatus",n),e.on("changeSelection",n),e.on("keyboardActivity",n)};(function(){this.updateStatus=function(e){function n(e,n){e&&t.push(e,n||"|")}var t=[];n(e.keyBinding.getStatusText(e)),e.commands.recording&&n("REC");var r=e.selection,i=r.lead;if(!r.isEmpty()){var s=e.getSelectionRange();n("("+(s.end.row-s.start.row)+":"+(s.end.column-s.start.column)+")"," ")}n(i.row+":"+i.column," "),r.rangeCount&&n("["+r.rangeCount+"]"," "),t.pop(),this.element.textContent=t.join("")}}).call(s.prototype),t.StatusBar=s}); (function() {
|
||||
ace.require(["ace/ext/statusbar"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/themelist",["require","exports","module"],function(e,t,n){"use strict";var r=[["Chrome"],["Clouds"],["Crimson Editor"],["Dawn"],["Dreamweaver"],["Eclipse"],["GitHub"],["IPlastic"],["Solarized Light"],["TextMate"],["Tomorrow"],["Xcode"],["Kuroir"],["KatzenMilch"],["SQL Server","sqlserver","light"],["Ambiance","ambiance","dark"],["Chaos","chaos","dark"],["Clouds Midnight","clouds_midnight","dark"],["Dracula","","dark"],["Cobalt","cobalt","dark"],["Gruvbox","gruvbox","dark"],["Green on Black","gob","dark"],["idle Fingers","idle_fingers","dark"],["krTheme","kr_theme","dark"],["Merbivore","merbivore","dark"],["Merbivore Soft","merbivore_soft","dark"],["Mono Industrial","mono_industrial","dark"],["Monokai","monokai","dark"],["Nord Dark","nord_dark","dark"],["Pastel on dark","pastel_on_dark","dark"],["Solarized Dark","solarized_dark","dark"],["Terminal","terminal","dark"],["Tomorrow Night","tomorrow_night","dark"],["Tomorrow Night Blue","tomorrow_night_blue","dark"],["Tomorrow Night Bright","tomorrow_night_bright","dark"],["Tomorrow Night 80s","tomorrow_night_eighties","dark"],["Twilight","twilight","dark"],["Vibrant Ink","vibrant_ink","dark"]];t.themesByName={},t.themes=r.map(function(e){var n=e[1]||e[0].replace(/ /g,"_").toLowerCase(),r={caption:e[0],theme:"ace/theme/"+n,isDark:e[2]=="dark",name:n};return t.themesByName[n]=r,r})}); (function() {
|
||||
ace.require(["ace/ext/themelist"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/ext/whitespace",["require","exports","module","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../lib/lang");t.$detectIndentation=function(e,t){function c(e){var t=0;for(var r=e;r<n.length;r+=e)t+=n[r]||0;return t}var n=[],r=[],i=0,s=0,o=Math.min(e.length,1e3);for(var u=0;u<o;u++){var a=e[u];if(!/^\s*[^*+\-\s]/.test(a))continue;if(a[0]==" ")i++,s=-Number.MAX_VALUE;else{var f=a.match(/^ */)[0].length;if(f&&a[f]!=" "){var l=f-s;l>0&&!(s%l)&&!(f%l)&&(r[l]=(r[l]||0)+1),n[f]=(n[f]||0)+1}s=f}while(u<o&&a[a.length-1]=="\\")a=e[u++]}var h=r.reduce(function(e,t){return e+t},0),p={score:0,length:0},d=0;for(var u=1;u<12;u++){var v=c(u);u==1?(d=v,v=n[1]?.9:.8,n.length||(v=0)):v/=d,r[u]&&(v+=r[u]/h),v>p.score&&(p={score:v,length:u})}if(p.score&&p.score>1.4)var m=p.length;if(i>d+1){if(m==1||d<i/4||p.score<1.8)m=undefined;return{ch:" ",length:m}}if(d>i+1)return{ch:" ",length:m}},t.detectIndentation=function(e){var n=e.getLines(0,1e3),r=t.$detectIndentation(n)||{};return r.ch&&e.setUseSoftTabs(r.ch==" "),r.length&&e.setTabSize(r.length),r},t.trimTrailingSpace=function(e,t){var n=e.getDocument(),r=n.getAllLines(),i=t&&t.trimEmpty?-1:0,s=[],o=-1;t&&t.keepCursorPosition&&(e.selection.rangeCount?e.selection.rangeList.ranges.forEach(function(e,t,n){var r=n[t+1];if(r&&r.cursor.row==e.cursor.row)return;s.push(e.cursor)}):s.push(e.selection.getCursor()),o=0);var u=s[o]&&s[o].row;for(var a=0,f=r.length;a<f;a++){var l=r[a],c=l.search(/\s+$/);a==u&&(c<s[o].column&&c>i&&(c=s[o].column),o++,u=s[o]?s[o].row:-1),c>i&&n.removeInLine(a,c,l.length)}},t.convertIndentation=function(e,t,n){var i=e.getTabString()[0],s=e.getTabSize();n||(n=s),t||(t=i);var o=t==" "?t:r.stringRepeat(t,n),u=e.doc,a=u.getAllLines(),f={},l={};for(var c=0,h=a.length;c<h;c++){var p=a[c],d=p.match(/^\s*/)[0];if(d){var v=e.$getStringScreenWidth(d)[0],m=Math.floor(v/s),g=v%s,y=f[m]||(f[m]=r.stringRepeat(o,m));y+=l[g]||(l[g]=r.stringRepeat(" ",g)),y!=d&&(u.removeInLine(c,0,d.length),u.insertInLine({row:c,column:0},y))}}e.setTabSize(n),e.setUseSoftTabs(t==" ")},t.$parseStringArg=function(e){var t={};/t/.test(e)?t.ch=" ":/s/.test(e)&&(t.ch=" ");var n=e.match(/\d+/);return n&&(t.length=parseInt(n[0],10)),t},t.$parseArg=function(e){return e?typeof e=="string"?t.$parseStringArg(e):typeof e.text=="string"?t.$parseStringArg(e.text):e:{}},t.commands=[{name:"detectIndentation",description:"Detect indentation from content",exec:function(e){t.detectIndentation(e.session)}},{name:"trimTrailingSpace",description:"Trim trailing whitespace",exec:function(e,n){t.trimTrailingSpace(e.session,n)}},{name:"convertIndentation",description:"Convert indentation to ...",exec:function(e,n){var r=t.$parseArg(n);t.convertIndentation(e.session,r.ch,r.length)}},{name:"setIndentation",description:"Set indentation",exec:function(e,n){var r=t.$parseArg(n);r.length&&e.session.setTabSize(r.length),r.ch&&e.session.setUseSoftTabs(r.ch==" ")}}]}); (function() {
|
||||
ace.require(["ace/ext/whitespace"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,8 +0,0 @@
|
|||
ace.define("ace/mode/yaml_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"comment",regex:"#.*$"},{token:"list.markup",regex:/^(?:-{3}|\.{3})\s*(?=#|$)/},{token:"list.markup",regex:/^\s*[\-?](?:$|\s)/},{token:"constant",regex:"!![\\w//]+"},{token:"constant.language",regex:"[&\\*][a-zA-Z0-9-_]+"},{token:["meta.tag","keyword"],regex:/^(\s*\w.*?)(:(?=\s|$))/},{token:["meta.tag","keyword"],regex:/(\w+?)(\s*:(?=\s|$))/},{token:"keyword.operator",regex:"<<\\w*:\\w*"},{token:"keyword.operator",regex:"-\\s*(?=[{])"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:/[|>][-+\d]*(?:$|\s+(?:$|#))/,onMatch:function(e,t,n,r){r=r.replace(/ #.*/,"");var i=/^ *((:\s*)?-(\s*[^|>])?)?/.exec(r)[0].replace(/\S\s*$/,"").length,s=parseInt(/\d+[\s+-]*$/.exec(r));return s?(i+=s-1,this.next="mlString"):this.next="mlStringPre",n.length?(n[0]=this.next,n[1]=i):(n.push(this.next),n.push(i)),this.token},next:"mlString"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"constant.numeric",regex:/(\b|[+\-\.])[\d_]+(?:(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)(?=[^\d-\w]|$)/},{token:"constant.numeric",regex:/[+\-]?\.inf\b|NaN\b|0x[\dA-Fa-f_]+|0b[10_]+/},{token:"constant.language.boolean",regex:"\\b(?:true|false|TRUE|FALSE|True|False|yes|no)\\b"},{token:"paren.lparen",regex:"[[({]"},{token:"paren.rparen",regex:"[\\])}]"},{token:"text",regex:/[^\s,:\[\]\{\}]+/}],mlStringPre:[{token:"indent",regex:/^ *$/},{token:"indent",regex:/^ */,onMatch:function(e,t,n){var r=n[1];return r>=e.length?(this.next="start",n.shift(),n.shift()):(n[1]=e.length-1,this.next=n[0]="mlString"),this.token},next:"mlString"},{defaultToken:"string"}],mlString:[{token:"indent",regex:/^ *$/},{token:"indent",regex:/^ */,onMatch:function(e,t,n){var r=n[1];return r>=e.length?(this.next="start",n.splice(0)):this.next="mlString",this.token},next:"mlString"},{token:"string",regex:".+"}]},this.normalizeRules()};r.inherits(s,i),t.YamlHighlightRules=s}),ace.define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../range").Range,i=function(){};(function(){this.checkOutdent=function(e,t){return/^\s+$/.test(e)?/^\s*\}/.test(t):!1},this.autoOutdent=function(e,t){var n=e.getLine(t),i=n.match(/^(\s*\})/);if(!i)return 0;var s=i[1].length,o=e.findMatchingBracket({row:t,column:s});if(!o||o.row==t)return 0;var u=this.$getIndent(e.getLine(o.row));e.replace(new r(t,0,t,s-1),u)},this.$getIndent=function(e){return e.match(/^\s*/)[0]}}).call(i.prototype),t.MatchingBraceOutdent=i}),ace.define("ace/mode/folding/coffee",["require","exports","module","ace/lib/oop","ace/mode/folding/fold_mode","ace/range"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("./fold_mode").FoldMode,s=e("../../range").Range,o=t.FoldMode=function(){};r.inherits(o,i),function(){this.getFoldWidgetRange=function(e,t,n){var r=this.indentationBlock(e,n);if(r)return r;var i=/\S/,o=e.getLine(n),u=o.search(i);if(u==-1||o[u]!="#")return;var a=o.length,f=e.getLength(),l=n,c=n;while(++n<f){o=e.getLine(n);var h=o.search(i);if(h==-1)continue;if(o[h]!="#")break;c=n}if(c>l){var p=e.getLine(c).length;return new s(l,a,c,p)}},this.getFoldWidget=function(e,t,n){var r=e.getLine(n),i=r.search(/\S/),s=e.getLine(n+1),o=e.getLine(n-1),u=o.search(/\S/),a=s.search(/\S/);if(i==-1)return e.foldWidgets[n-1]=u!=-1&&u<a?"start":"","";if(u==-1){if(i==a&&r[i]=="#"&&s[i]=="#")return e.foldWidgets[n-1]="",e.foldWidgets[n+1]="","start"}else if(u==i&&r[i]=="#"&&o[i]=="#"&&e.getLine(n-2).search(/\S/)==-1)return e.foldWidgets[n-1]="start",e.foldWidgets[n+1]="","";return u!=-1&&u<i?e.foldWidgets[n-1]="start":e.foldWidgets[n-1]="",i<a?"start":""}}.call(o.prototype)}),ace.define("ace/mode/yaml",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/yaml_highlight_rules","ace/mode/matching_brace_outdent","ace/mode/folding/coffee"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./yaml_highlight_rules").YamlHighlightRules,o=e("./matching_brace_outdent").MatchingBraceOutdent,u=e("./folding/coffee").FoldMode,a=function(){this.HighlightRules=s,this.$outdent=new o,this.foldingRules=new u,this.$behaviour=this.$defaultBehaviour};r.inherits(a,i),function(){this.lineCommentStart=["#"],this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t);if(e=="start"){var i=t.match(/^.*[\{\(\[]\s*$/);i&&(r+=n)}return r},this.checkOutdent=function(e,t,n){return this.$outdent.checkOutdent(t,n)},this.autoOutdent=function(e,t,n){this.$outdent.autoOutdent(t,n)},this.$id="ace/mode/yaml"}.call(a.prototype),t.Mode=a}); (function() {
|
||||
ace.require(["ace/mode/yaml"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
; (function() {
|
||||
ace.require(["ace/snippets/yaml"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -10,11 +10,12 @@
|
|||
<table class="table card-table table-responsive table-responsive-large" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 35%">Name</th>
|
||||
<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%">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>
|
||||
|
@ -27,6 +28,7 @@
|
|||
<td class="d-none d-lg-table-cell">
|
||||
{{{torrent_status pieceChunks totalPieces}}}
|
||||
</td>
|
||||
<td><i class="mdi mdi-delete-forever" title="delete torrent" onclick='Distribyted.routes.deleteTorrent("{{../name}}","{{hash}}")'></i></td>
|
||||
</tr>
|
||||
{{/torrentStats}}
|
||||
</tbody>
|
||||
|
|
|
@ -4,17 +4,18 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/missinggo/v2/filecache"
|
||||
t "github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"github.com/distribyted/distribyted/config"
|
||||
"github.com/distribyted/distribyted/fs"
|
||||
"github.com/distribyted/distribyted/fuse"
|
||||
"github.com/distribyted/distribyted/http"
|
||||
"github.com/distribyted/distribyted/stats"
|
||||
"github.com/distribyted/distribyted/torrent"
|
||||
"github.com/distribyted/distribyted/torrent/loader"
|
||||
"github.com/distribyted/distribyted/webdav"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -78,14 +79,6 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func newCache(folder string) (*filecache.Cache, error) {
|
||||
if err := os.MkdirAll(folder, 0744); err != nil {
|
||||
return nil, fmt.Errorf("error creating metadata folder: %w", err)
|
||||
}
|
||||
|
||||
return filecache.NewCache(folder)
|
||||
}
|
||||
|
||||
func load(configPath string, port, webDAVPort int, fuseAllowOther bool) error {
|
||||
ch := config.NewHandler(configPath)
|
||||
|
||||
|
@ -94,21 +87,36 @@ func load(configPath string, port, webDAVPort int, fuseAllowOther bool) error {
|
|||
return fmt.Errorf("error loading configuration: %w", err)
|
||||
}
|
||||
|
||||
fc, err := newCache(conf.Torrent.MetadataFolder)
|
||||
if err := os.MkdirAll(conf.Torrent.MetadataFolder, 0744); err != nil {
|
||||
return fmt.Errorf("error creating metadata folder: %w", err)
|
||||
}
|
||||
|
||||
fc, err := filecache.NewCache(path.Join(conf.Torrent.MetadataFolder, "cache"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating cache: %w", err)
|
||||
}
|
||||
|
||||
st := storage.NewResourcePieces(fc.AsResourceProvider())
|
||||
|
||||
c, err := torrent.NewClient(st, conf.Torrent)
|
||||
fis, err := torrent.NewFileItemStore(path.Join(conf.Torrent.MetadataFolder, "items"), 2*time.Hour)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error starting item store: %w", err)
|
||||
}
|
||||
|
||||
c, err := torrent.NewClient(st, fis, conf.Torrent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error starting torrent client: %w", err)
|
||||
}
|
||||
|
||||
ss := stats.NewTorrent()
|
||||
cl := loader.NewConfig(conf.Routes)
|
||||
ss := torrent.NewStats()
|
||||
|
||||
th := torrent.NewHandler(c, ss)
|
||||
dbl, err := loader.NewDB(path.Join(conf.Torrent.MetadataFolder, "magnetdb"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error starting magnet database: %w", err)
|
||||
}
|
||||
|
||||
ts := torrent.NewService(cl, dbl, ss, c)
|
||||
|
||||
mh := fuse.NewHandler(fuseAllowOther || conf.Fuse.AllowOther, conf.Fuse.Path)
|
||||
|
||||
|
@ -116,36 +124,33 @@ func load(configPath string, port, webDAVPort int, fuseAllowOther bool) error {
|
|||
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
|
||||
<-sigChan
|
||||
tryClose(c, mh)
|
||||
log.Info().Msg("closing items database...")
|
||||
fis.Close()
|
||||
log.Info().Msg("closing magnet database...")
|
||||
dbl.Close()
|
||||
log.Info().Msg("closing torrent client...")
|
||||
c.Close()
|
||||
log.Info().Msg("unmounting fuse filesystem...")
|
||||
mh.Unmount()
|
||||
|
||||
log.Info().Msg("exiting")
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
ch.OnReload(func(c *config.Root, ef config.EventFunc) error {
|
||||
ef("unmounting filesystems")
|
||||
mh.Unmount()
|
||||
th.RemoveAll()
|
||||
log.Info().Msg(fmt.Sprintf("setting cache size to %d MB", conf.Torrent.GlobalCacheSize))
|
||||
fc.SetCapacity(conf.Torrent.GlobalCacheSize * 1024 * 1024)
|
||||
|
||||
ef(fmt.Sprintf("setting cache size to %d MB", c.Torrent.GlobalCacheSize))
|
||||
fc.SetCapacity(c.Torrent.GlobalCacheSize * 1024 * 1024)
|
||||
|
||||
for _, mp := range c.Routes {
|
||||
ef(fmt.Sprintf("loading %v with %d torrents...", mp.Name, len(mp.Torrents)))
|
||||
if err := th.Load(mp.Name, mp.Torrents); err != nil {
|
||||
return fmt.Errorf("error loading route %v: %w", mp.Name, err)
|
||||
}
|
||||
ef(fmt.Sprintf("%v loaded", mp.Name))
|
||||
fss, err := ts.Load()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when loading torrents: %w", err)
|
||||
}
|
||||
|
||||
return mh.Mount(th.Fileststems(), ef)
|
||||
|
||||
})
|
||||
|
||||
if err := ch.Reload(nil); err != nil {
|
||||
return fmt.Errorf("error reloading configuration: %w", err)
|
||||
go func() {
|
||||
if err := mh.Mount(fss); err != nil {
|
||||
log.Info().Err(err).Msg("error mounting filesystems")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
tryClose(c, mh)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
@ -155,7 +160,7 @@ func load(configPath string, port, webDAVPort int, fuseAllowOther bool) error {
|
|||
port = conf.WebDAV.Port
|
||||
}
|
||||
|
||||
cfs, err := fs.NewContainerFs(th.Fileststems())
|
||||
cfs, err := fs.NewContainerFs(fss)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error adding files to webDAV")
|
||||
return
|
||||
|
@ -169,19 +174,7 @@ func load(configPath string, port, webDAVPort int, fuseAllowOther bool) error {
|
|||
log.Warn().Msg("webDAV configuration not found!")
|
||||
}()
|
||||
|
||||
err = http.New(fc, ss, ch, port)
|
||||
|
||||
err = http.New(fc, ss, ts, ch, port)
|
||||
log.Error().Err(err).Msg("error initializing HTTP server")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func tryClose(c *t.Client, mountService *fuse.Handler) {
|
||||
log.Info().Msg("closing torrent client...")
|
||||
c.Close()
|
||||
log.Info().Msg("unmounting fuse filesystem...")
|
||||
mountService.Unmount()
|
||||
|
||||
log.Info().Msg("exiting")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ type ReloadFunc func(*Root, EventFunc) error
|
|||
|
||||
type Handler struct {
|
||||
p string
|
||||
reloadFunc ReloadFunc
|
||||
}
|
||||
|
||||
func NewHandler(path string) *Handler {
|
||||
|
@ -71,27 +70,6 @@ func (c *Handler) Get() (*Root, error) {
|
|||
return conf, nil
|
||||
}
|
||||
|
||||
func (c *Handler) OnReload(reloadFunc ReloadFunc) {
|
||||
c.reloadFunc = reloadFunc
|
||||
}
|
||||
|
||||
func (c *Handler) Reload(ef EventFunc) error {
|
||||
if ef == nil {
|
||||
ef = func(string) {}
|
||||
}
|
||||
|
||||
conf, err := c.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.reloadFunc != nil {
|
||||
return c.reloadFunc(conf, ef)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Handler) Set(b []byte) error {
|
||||
if err := yaml.Unmarshal(b, &Root{}); err != nil {
|
||||
return fmt.Errorf("error parsing configuration file: %w", err)
|
||||
|
|
|
@ -26,13 +26,19 @@ type storage struct {
|
|||
|
||||
func newStorage(factories map[string]FsFactory) *storage {
|
||||
return &storage{
|
||||
files: make(map[string]File, 0),
|
||||
children: make(map[string]map[string]File, 0),
|
||||
filesystems: make(map[string]Filesystem, 0),
|
||||
files: make(map[string]File),
|
||||
children: make(map[string]map[string]File),
|
||||
filesystems: make(map[string]Filesystem),
|
||||
factories: factories,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *storage) Clear() {
|
||||
s.files = make(map[string]File)
|
||||
s.children = make(map[string]map[string]File)
|
||||
s.filesystems = make(map[string]Filesystem)
|
||||
}
|
||||
|
||||
func (s *storage) Has(path string) bool {
|
||||
path = clean(path)
|
||||
|
||||
|
@ -100,7 +106,7 @@ func (s *storage) createParent(p string, f File) error {
|
|||
}
|
||||
|
||||
if _, ok := s.children[base]; !ok {
|
||||
s.children[base] = make(map[string]File, 0)
|
||||
s.children[base] = make(map[string]File)
|
||||
}
|
||||
|
||||
if filename != "" {
|
||||
|
@ -118,7 +124,7 @@ func (s *storage) Children(path string) map[string]File {
|
|||
return out
|
||||
}
|
||||
|
||||
l := make(map[string]File, 0)
|
||||
l := make(map[string]File)
|
||||
for n, f := range s.children[path] {
|
||||
l[n] = f
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/distribyted/distribyted/iio"
|
||||
)
|
||||
|
@ -8,18 +10,45 @@ import (
|
|||
var _ Filesystem = &Torrent{}
|
||||
|
||||
type Torrent struct {
|
||||
ts []*torrent.Torrent
|
||||
mu sync.Mutex
|
||||
ts map[string]*torrent.Torrent
|
||||
s *storage
|
||||
loaded bool
|
||||
}
|
||||
|
||||
func NewTorrent(ts []*torrent.Torrent) *Torrent {
|
||||
func NewTorrent() *Torrent {
|
||||
return &Torrent{
|
||||
ts: ts,
|
||||
s: newStorage(SupportedFactories),
|
||||
ts: make(map[string]*torrent.Torrent),
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *Torrent) AddTorrent(t *torrent.Torrent) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
fs.loaded = false
|
||||
fs.ts[t.InfoHash().HexString()] = t
|
||||
}
|
||||
|
||||
func (fs *Torrent) RemoveTorrent(h string) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
|
||||
fs.s.Clear()
|
||||
|
||||
fs.loaded = false
|
||||
|
||||
delete(fs.ts, h)
|
||||
}
|
||||
|
||||
func (fs *Torrent) load() {
|
||||
if fs.loaded {
|
||||
return
|
||||
}
|
||||
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
|
||||
for _, t := range fs.ts {
|
||||
<-t.GotInfo()
|
||||
for _, file := range t.Files() {
|
||||
|
@ -27,6 +56,7 @@ func (fs *Torrent) load() {
|
|||
}
|
||||
}
|
||||
|
||||
fs.loaded = true
|
||||
}
|
||||
|
||||
func (fs *Torrent) Open(filename string) (File, error) {
|
||||
|
|
|
@ -25,7 +25,8 @@ func TestTorrentFilesystem(t *testing.T) {
|
|||
to, err := client.AddMagnet(testMagnet)
|
||||
require.NoError(err)
|
||||
|
||||
tfs := NewTorrent([]*torrent.Torrent{to})
|
||||
tfs := NewTorrent()
|
||||
tfs.AddTorrent(to)
|
||||
|
||||
files, err := tfs.ReadDir("/")
|
||||
require.NoError(err)
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"runtime"
|
||||
|
||||
"github.com/billziss-gh/cgofuse/fuse"
|
||||
"github.com/distribyted/distribyted/config"
|
||||
"github.com/distribyted/distribyted/fs"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
@ -25,7 +24,7 @@ func NewHandler(fuseAllowOther bool, path string) *Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Handler) Mount(fss map[string]fs.Filesystem, ef config.EventFunc) error {
|
||||
func (s *Handler) Mount(fss map[string]fs.Filesystem) error {
|
||||
folder := s.path
|
||||
// On windows, the folder must don't exist
|
||||
if runtime.GOOS == "windows" {
|
||||
|
@ -58,6 +57,8 @@ func (s *Handler) Mount(fss map[string]fs.Filesystem, ef config.EventFunc) error
|
|||
|
||||
s.host = host
|
||||
|
||||
log.Info().Str("path", folder).Msg("starting FUSE mount")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
48
go.mod
48
go.mod
|
@ -4,12 +4,14 @@ go 1.17
|
|||
|
||||
require (
|
||||
github.com/RoaringBitmap/roaring v0.9.4 // indirect
|
||||
github.com/anacrolix/log v0.9.0
|
||||
github.com/anacrolix/dht/v2 v2.12.1
|
||||
github.com/anacrolix/log v0.10.0
|
||||
github.com/anacrolix/missinggo/v2 v2.5.2
|
||||
github.com/anacrolix/multiless v0.2.0 // indirect
|
||||
github.com/anacrolix/torrent v1.33.0
|
||||
github.com/anacrolix/torrent v1.37.0
|
||||
github.com/billziss-gh/cgofuse v1.5.0
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2
|
||||
github.com/elliotchance/orderedmap v1.4.0 // indirect
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gin-gonic/gin v1.7.4
|
||||
|
@ -19,16 +21,16 @@ require (
|
|||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/pion/dtls/v2 v2.0.10 // indirect
|
||||
github.com/pion/mdns v0.0.5 // indirect
|
||||
github.com/pion/rtp v1.7.2 // indirect
|
||||
github.com/pion/sctp v1.7.12 // indirect
|
||||
github.com/pion/rtp v1.7.4 // indirect
|
||||
github.com/pion/sctp v1.8.0 // indirect
|
||||
github.com/rs/zerolog v1.25.0
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/net v0.0.0-20211007125505-59d4e928ea9d
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
|
||||
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
|
@ -36,9 +38,8 @@ require (
|
|||
|
||||
require (
|
||||
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 // indirect
|
||||
github.com/anacrolix/chansync v0.3.0-0.0.20211007004133-3f72684c4a93 // indirect
|
||||
github.com/anacrolix/confluence v1.8.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.10.6-0.20211007004332-99263ec9c1c8 // indirect
|
||||
github.com/anacrolix/chansync v0.3.0 // indirect
|
||||
github.com/anacrolix/confluence v1.10.0 // indirect
|
||||
github.com/anacrolix/envpprof v1.1.1 // indirect
|
||||
github.com/anacrolix/go-libutp v1.0.4 // indirect
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
|
@ -49,38 +50,48 @@ require (
|
|||
github.com/anacrolix/upnp v0.1.2-0.20200416075019-5e9378ed1425 // indirect
|
||||
github.com/anacrolix/utp v0.1.0 // indirect
|
||||
github.com/benbjohnson/immutable v0.3.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.1 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/edsrzf/mmap-go v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/golang/snappy v0.0.3 // indirect
|
||||
github.com/google/flatbuffers v1.12.1 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/json-iterator/go v1.1.10 // indirect
|
||||
github.com/klauspost/compress v1.12.3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/pion/datachannel v1.4.21 // indirect
|
||||
github.com/pion/ice/v2 v2.1.12 // indirect
|
||||
github.com/pion/interceptor v0.0.15 // indirect
|
||||
github.com/pion/datachannel v1.5.2 // indirect
|
||||
github.com/pion/ice/v2 v2.1.13 // indirect
|
||||
github.com/pion/interceptor v0.1.0 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.6 // indirect
|
||||
github.com/pion/rtcp v1.2.8 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.4 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.5 // indirect
|
||||
github.com/pion/stun v0.3.5 // indirect
|
||||
github.com/pion/transport v0.12.3 // indirect
|
||||
github.com/pion/turn/v2 v2.0.5 // indirect
|
||||
github.com/pion/udp v0.1.1 // indirect
|
||||
github.com/pion/webrtc/v3 v3.0.32 // indirect
|
||||
github.com/pion/webrtc/v3 v3.1.9 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
|
@ -88,8 +99,9 @@ require (
|
|||
github.com/willf/bitset v1.1.11 // indirect
|
||||
github.com/willf/bloom v2.0.3+incompatible // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.opencensus.io v0.22.5 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
)
|
||||
|
|
88
http/api.go
88
http/api.go
|
@ -1,19 +1,15 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/anacrolix/missinggo/v2/filecache"
|
||||
"github.com/distribyted/distribyted/config"
|
||||
"github.com/distribyted/distribyted/stats"
|
||||
"github.com/distribyted/distribyted/torrent"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var apiStatusHandler = func(fc *filecache.Cache, ss *stats.Torrent) gin.HandlerFunc {
|
||||
var apiStatusHandler = func(fc *filecache.Cache, ss *torrent.Stats) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
// TODO move to a struct
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
|
@ -25,81 +21,43 @@ var apiStatusHandler = func(fc *filecache.Cache, ss *stats.Torrent) gin.HandlerF
|
|||
}
|
||||
}
|
||||
|
||||
var apiRoutesHandler = func(ss *stats.Torrent) gin.HandlerFunc {
|
||||
var apiRoutesHandler = func(ss *torrent.Stats) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
s := ss.RoutesStats()
|
||||
sort.Sort(stats.ByName(s))
|
||||
sort.Sort(torrent.ByName(s))
|
||||
ctx.JSON(http.StatusOK, s)
|
||||
}
|
||||
}
|
||||
|
||||
var apiGetConfigFile = func(ch *config.Handler) gin.HandlerFunc {
|
||||
var apiAddTorrentHandler = func(s *torrent.Service) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
rc, err := ch.GetRaw()
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
route := ctx.Param("route")
|
||||
|
||||
var json RouteAdd
|
||||
if err := ctx.ShouldBindJSON(&json); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data(http.StatusOK, "text/x-yaml", rc)
|
||||
if err := s.AddMagnet(route, json.Magnet); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, nil)
|
||||
}
|
||||
}
|
||||
|
||||
var apiSetConfigFile = func(ch *config.Handler) gin.HandlerFunc {
|
||||
var apiDelTorrentHandler = func(s *torrent.Service) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
if ctx.Request.Body == nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, errors.New("no config file sent"))
|
||||
route := ctx.Param("route")
|
||||
hash := ctx.Param("torrent_hash")
|
||||
|
||||
if err := s.RemoveFromHash(route, hash); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c, err := ioutil.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(c) == 0 {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, errors.New("no config file sent"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := ch.Set(c); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"message": "config file saved",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var apiStreamEvents = func(events chan string) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
ctx.Stream(func(w io.Writer) bool {
|
||||
if msg, ok := <-events; ok {
|
||||
ctx.SSEvent("event", msg)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var apiReloadServer = func(ch *config.Handler, events chan string) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
events <- "starting reload configuration process..."
|
||||
|
||||
err := ch.Reload(
|
||||
func(m string) {
|
||||
events <- m
|
||||
})
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"message": "reload process finished with no errors",
|
||||
})
|
||||
ctx.JSON(http.StatusOK, nil)
|
||||
}
|
||||
}
|
||||
|
|
14
http/http.go
14
http/http.go
|
@ -6,14 +6,14 @@ import (
|
|||
"github.com/anacrolix/missinggo/v2/filecache"
|
||||
"github.com/distribyted/distribyted"
|
||||
"github.com/distribyted/distribyted/config"
|
||||
"github.com/distribyted/distribyted/stats"
|
||||
"github.com/distribyted/distribyted/torrent"
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/shurcooL/httpfs/html/vfstemplate"
|
||||
)
|
||||
|
||||
func New(fc *filecache.Cache, ss *stats.Torrent, ch *config.Handler, port int) error {
|
||||
func New(fc *filecache.Cache, ss *torrent.Stats, s *torrent.Service, ch *config.Handler, port int) error {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
@ -30,18 +30,14 @@ func New(fc *filecache.Cache, ss *stats.Torrent, ch *config.Handler, port int) e
|
|||
|
||||
r.GET("/", indexHandler)
|
||||
r.GET("/routes", routesHandler(ss))
|
||||
r.GET("/config", configHandler)
|
||||
|
||||
eventChan := make(chan string)
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
api.GET("/status", apiStatusHandler(fc, ss))
|
||||
api.GET("/routes", apiRoutesHandler(ss))
|
||||
api.GET("/config", apiGetConfigFile(ch))
|
||||
api.POST("/config", apiSetConfigFile(ch))
|
||||
api.POST("/reload", apiReloadServer(ch, eventChan))
|
||||
api.GET("/events", apiStreamEvents(eventChan))
|
||||
api.POST("/routes/:route/torrent", apiAddTorrentHandler(s))
|
||||
api.DELETE("/routes/:route/torrent/:torrent_hash", apiDelTorrentHandler(s))
|
||||
|
||||
}
|
||||
|
||||
log.Info().Str("host", fmt.Sprintf("0.0.0.0:%d", port)).Msg("starting webserver")
|
||||
|
|
9
http/model.go
Normal file
9
http/model.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package http
|
||||
|
||||
type RouteAdd struct {
|
||||
Magnet string `json:"magnet" binding:"required"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Error string `json:"error"`
|
||||
}
|
|
@ -3,7 +3,7 @@ package http
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/distribyted/distribyted/stats"
|
||||
"github.com/distribyted/distribyted/torrent"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
@ -11,12 +11,8 @@ var indexHandler = func(c *gin.Context) {
|
|||
c.HTML(http.StatusOK, "index.html", nil)
|
||||
}
|
||||
|
||||
var routesHandler = func(ss *stats.Torrent) gin.HandlerFunc {
|
||||
var routesHandler = func(ss *torrent.Stats) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "routes.html", ss.RoutesStats())
|
||||
}
|
||||
}
|
||||
|
||||
var configHandler = func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "config.html", nil)
|
||||
}
|
||||
|
|
27
log/badger.go
Normal file
27
log/badger.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Badger struct {
|
||||
L zerolog.Logger
|
||||
}
|
||||
|
||||
func (l *Badger) Errorf(m string, f ...interface{}) {
|
||||
l.L.Error().Msgf(strings.ReplaceAll(m, "\n", ""), f...)
|
||||
}
|
||||
|
||||
func (l *Badger) Warningf(m string, f ...interface{}) {
|
||||
l.L.Warn().Msgf(strings.ReplaceAll(m, "\n", ""), f...)
|
||||
}
|
||||
|
||||
func (l *Badger) Infof(m string, f ...interface{}) {
|
||||
l.L.Info().Msgf(strings.ReplaceAll(m, "\n", ""), f...)
|
||||
}
|
||||
|
||||
func (l *Badger) Debugf(m string, f ...interface{}) {
|
||||
l.L.Debug().Msgf(strings.ReplaceAll(m, "\n", ""), f...)
|
||||
}
|
39
log/torrent.go
Normal file
39
log/torrent.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"github.com/anacrolix/log"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var _ log.LoggerImpl = &Torrent{}
|
||||
|
||||
type Torrent struct {
|
||||
L zerolog.Logger
|
||||
}
|
||||
|
||||
func (l *Torrent) Log(m log.Msg) {
|
||||
level, ok := m.GetLevel()
|
||||
|
||||
e := l.L.Info()
|
||||
|
||||
if !ok {
|
||||
level = log.Debug
|
||||
}
|
||||
|
||||
switch level {
|
||||
case log.Debug:
|
||||
e = l.L.Debug()
|
||||
case log.Info:
|
||||
e = l.L.Debug().Str("error-type", "info")
|
||||
case log.Warning:
|
||||
e = l.L.Warn()
|
||||
case log.Error:
|
||||
e = l.L.Warn().Str("error-type", "error")
|
||||
case log.Critical:
|
||||
e = l.L.Warn().Str("error-type", "critical")
|
||||
case log.Fatal:
|
||||
e = l.L.Warn().Str("error-type", "fatal")
|
||||
}
|
||||
|
||||
e.Msgf(m.String())
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
{{template "header.html" "Configuration"}}
|
||||
<style>
|
||||
#editor {
|
||||
/** Setting height is also important, otherwise editor wont showup**/
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
||||
<body class="header-fixed sidebar-fixed sidebar-dark header-light" id="body">
|
||||
<div class="wrapper">
|
||||
{{template "navbar.html" "config"}}
|
||||
|
||||
<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">
|
||||
<!-- Large Modal -->
|
||||
<div class="modal fade" id="distribyted-reload-info-modal" tabindex="-1" role="dialog"
|
||||
aria-labelledby="distribyted-reload-info-modal" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="distribyted-reload-info-modal-title">Reload status</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul id="distribyted-reload-info-text" class="list-group"></ul>
|
||||
<div class="card-body d-flex align-items-center justify-content-center">
|
||||
<div class="sk-wave" id="distribyted-reload-info-loading"
|
||||
style="display:none">
|
||||
<div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger btn-pill"
|
||||
data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="card card-default">
|
||||
<div class="card-header card-header-border-bottom">
|
||||
<h2>Yaml Editor</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<div class="form-footer pt-4 pt-5 mt-4 border-top">
|
||||
<button type="button" class="btn btn-primary btn-default"
|
||||
onclick="javascript:Distribyted.config.save()">Save</button>
|
||||
<button type="button" class="btn btn-secondary btn-default"
|
||||
data-toggle="modal" data-target="#distribyted-reload-info-modal"
|
||||
onclick="javascript:Distribyted.config.reload()">Reload server</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer mt-auto">
|
||||
<div class="copyright bg-white">
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
{{template "footer.html"}}
|
||||
|
||||
<script src="assets/plugins/ace/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="assets/plugins/ace/ext-language_tools.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="assets/plugins/js-yaml/js-yaml.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="assets/js/config.js" type="text/javascript" charset="utf-8"></script>
|
||||
|
||||
<script>
|
||||
Distribyted.config.loadView();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -37,16 +37,16 @@
|
|||
<span class="nav-text">Routes</span>
|
||||
</a>
|
||||
</li>
|
||||
{{if eq . "config"}}
|
||||
<!-- {{if eq . "watched-folders"}}
|
||||
<li class="active">
|
||||
{{else}}
|
||||
<li>
|
||||
{{end}}
|
||||
<a class="sidenav-item-link" href="/config">
|
||||
<i class="mdi mdi-book-open-page-variant"></i>
|
||||
<span class="nav-text">Yaml Config</span>
|
||||
<a class="sidenav-item-link" href="/watched">
|
||||
<i class="mdi mdi-folder-upload-outline"></i>
|
||||
<span class="nav-text">Watched Folders</span>
|
||||
</a>
|
||||
</li>
|
||||
</li> -->
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -21,8 +21,36 @@
|
|||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="content" id="template_target">
|
||||
<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-footer pt-4 pt-5 mt-4 border-top">
|
||||
<button type="submit" class="btn btn-primary btn-default">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer mt-auto">
|
||||
<div class="copyright bg-white">
|
||||
</div>
|
||||
|
|
|
@ -1,20 +1,34 @@
|
|||
package torrent
|
||||
|
||||
import (
|
||||
"github.com/anacrolix/log"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/dht/v2"
|
||||
"github.com/anacrolix/dht/v2/bep44"
|
||||
tlog "github.com/anacrolix/log"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"github.com/distribyted/distribyted/config"
|
||||
dlog "github.com/distribyted/distribyted/log"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func NewClient(st storage.ClientImpl, cfg *config.TorrentGlobal) (*torrent.Client, error) {
|
||||
func NewClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentGlobal) (*torrent.Client, error) {
|
||||
// TODO download and upload limits
|
||||
torrentCfg := torrent.NewDefaultClientConfig()
|
||||
torrentCfg.Logger = log.Discard
|
||||
torrentCfg.Seed = true
|
||||
// torrentCfg.DisableWebseeds = true
|
||||
torrentCfg.DefaultStorage = st
|
||||
|
||||
torrentCfg.DisableIPv6 = cfg.DisableIPv6
|
||||
|
||||
l := log.Logger.With().Str("component", "torrent-client").Logger()
|
||||
torrentCfg.Logger = tlog.Logger{LoggerImpl: &dlog.Torrent{L: l}}
|
||||
|
||||
torrentCfg.ConfigureAnacrolixDhtServer = func(cfg *dht.ServerConfig) {
|
||||
cfg.Store = fis
|
||||
cfg.Exp = 2 * time.Hour
|
||||
cfg.NoSecurity = false
|
||||
}
|
||||
|
||||
return torrent.NewClient(torrentCfg)
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
package torrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/distribyted/distribyted/config"
|
||||
"github.com/distribyted/distribyted/fs"
|
||||
"github.com/distribyted/distribyted/stats"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
c *torrent.Client
|
||||
s *stats.Torrent
|
||||
|
||||
fssMu sync.Mutex
|
||||
fss map[string]fs.Filesystem
|
||||
}
|
||||
|
||||
func NewHandler(c *torrent.Client, s *stats.Torrent) *Handler {
|
||||
return &Handler{
|
||||
c: c,
|
||||
s: s,
|
||||
fss: make(map[string]fs.Filesystem),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Handler) Load(route string, ts []*config.Torrent) error {
|
||||
var torrents []*torrent.Torrent
|
||||
for _, mpcTorrent := range ts {
|
||||
var t *torrent.Torrent
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case mpcTorrent.MagnetURI != "":
|
||||
t, err = s.c.AddMagnet(mpcTorrent.MagnetURI)
|
||||
case mpcTorrent.TorrentPath != "":
|
||||
t, err = s.c.AddTorrentFromFile(mpcTorrent.TorrentPath)
|
||||
default:
|
||||
err = fmt.Errorf("no magnet URI or torrent path provided")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// only get info if name is not available
|
||||
if t.Name() == "" {
|
||||
log.Info().Str("hash", t.InfoHash().String()).Msg("getting torrent info")
|
||||
<-t.GotInfo()
|
||||
}
|
||||
|
||||
s.s.Add(route, t)
|
||||
torrents = append(torrents, t)
|
||||
|
||||
log.Info().Str("name", t.Name()).Str("route", route).Msg("torrent added to mountpoint")
|
||||
}
|
||||
|
||||
folder := "/" + route
|
||||
|
||||
s.fssMu.Lock()
|
||||
defer s.fssMu.Unlock()
|
||||
s.fss[folder] = fs.NewTorrent(torrents)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Handler) Fileststems() map[string]fs.Filesystem {
|
||||
return s.fss
|
||||
}
|
||||
|
||||
func (s *Handler) RemoveAll() error {
|
||||
s.fssMu.Lock()
|
||||
defer s.fssMu.Unlock()
|
||||
|
||||
s.fss = make(map[string]fs.Filesystem)
|
||||
s.s.RemoveAll()
|
||||
return nil
|
||||
}
|
45
torrent/loader/config.go
Normal file
45
torrent/loader/config.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package loader
|
||||
|
||||
import "github.com/distribyted/distribyted/config"
|
||||
|
||||
var _ Loader = &Config{}
|
||||
|
||||
type Config struct {
|
||||
c []*config.Route
|
||||
}
|
||||
|
||||
func NewConfig(r []*config.Route) *Config {
|
||||
return &Config{
|
||||
c: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Config) ListMagnets() (map[string][]string, error) {
|
||||
out := make(map[string][]string)
|
||||
for _, r := range l.c {
|
||||
for _, t := range r.Torrents {
|
||||
if t.MagnetURI == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
out[r.Name] = append(out[r.Name], t.MagnetURI)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (l *Config) ListTorrentPaths() (map[string][]string, error) {
|
||||
out := make(map[string][]string)
|
||||
for _, r := range l.c {
|
||||
for _, t := range r.Torrents {
|
||||
if t.TorrentPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
out[r.Name] = append(out[r.Name], t.TorrentPath)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
107
torrent/loader/db.go
Normal file
107
torrent/loader/db.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
dlog "github.com/distribyted/distribyted/log"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var _ LoaderAdder = &DB{}
|
||||
|
||||
const routeRootKey = "/route/"
|
||||
|
||||
type DB struct {
|
||||
db *badger.DB
|
||||
}
|
||||
|
||||
func NewDB(path string) (*DB, error) {
|
||||
l := log.Logger.With().Str("component", "torrent-store").Logger()
|
||||
db, err := badger.Open(badger.DefaultOptions(path).WithLogger(&dlog.Badger{L: l}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.RunValueLogGC(0.5)
|
||||
if err != nil && err != badger.ErrNoRewrite {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DB{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *DB) AddMagnet(r, m string) error {
|
||||
err := l.db.Update(func(txn *badger.Txn) error {
|
||||
spec, err := metainfo.ParseMagnetUri(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ih := spec.InfoHash.HexString()
|
||||
|
||||
rp := path.Join(routeRootKey, ih, r)
|
||||
return txn.Set([]byte(rp), []byte(m))
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.db.Sync()
|
||||
}
|
||||
|
||||
func (l *DB) RemoveFromHash(r, h string) (bool, error) {
|
||||
tx := l.db.NewTransaction(true)
|
||||
defer tx.Discard()
|
||||
|
||||
var mh metainfo.Hash
|
||||
if err := mh.FromHexString(h); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
rp := path.Join(routeRootKey, h, r)
|
||||
if _, err := tx.Get([]byte(rp)); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := tx.Delete([]byte(rp)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, tx.Commit()
|
||||
}
|
||||
|
||||
func (l *DB) ListMagnets() (map[string][]string, error) {
|
||||
tx := l.db.NewTransaction(false)
|
||||
defer tx.Discard()
|
||||
|
||||
it := tx.NewIterator(badger.DefaultIteratorOptions)
|
||||
defer it.Close()
|
||||
|
||||
prefix := []byte(routeRootKey)
|
||||
out := make(map[string][]string)
|
||||
for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
|
||||
_, r := path.Split(string(it.Item().Key()))
|
||||
i := it.Item()
|
||||
if err := i.Value(func(v []byte) error {
|
||||
out[r] = append(out[r], string(v))
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (l *DB) ListTorrentPaths() (map[string][]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (l *DB) Close() error {
|
||||
return l.db.Close()
|
||||
}
|
62
torrent/loader/db_test.go
Normal file
62
torrent/loader/db_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package loader
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const m1 = "magnet:?xt=urn:btih:c9e15763f722f23e98a29decdfae341b98d53056"
|
||||
|
||||
func TestDB(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
tmpService, err := os.MkdirTemp("", "service")
|
||||
require.NoError(err)
|
||||
tmpStorage, err := os.MkdirTemp("", "storage")
|
||||
require.NoError(err)
|
||||
|
||||
cs := storage.NewFile(tmpStorage)
|
||||
defer cs.Close()
|
||||
|
||||
s, err := NewDB(tmpService)
|
||||
require.NoError(err)
|
||||
defer s.Close()
|
||||
|
||||
err = s.AddMagnet("route1", "WRONG MAGNET")
|
||||
require.Error(err)
|
||||
|
||||
err = s.AddMagnet("route1", m1)
|
||||
require.NoError(err)
|
||||
|
||||
err = s.AddMagnet("route2", m1)
|
||||
require.NoError(err)
|
||||
|
||||
l, err := s.ListMagnets()
|
||||
require.NoError(err)
|
||||
require.Len(l, 2)
|
||||
require.Len(l["route1"], 1)
|
||||
require.Equal(l["route1"][0], m1)
|
||||
require.Len(l["route2"], 1)
|
||||
require.Equal(l["route2"][0], m1)
|
||||
|
||||
removed, err := s.RemoveFromHash("other", "c9e15763f722f23e98a29decdfae341b98d53056")
|
||||
require.NoError(err)
|
||||
require.False(removed)
|
||||
|
||||
removed, err = s.RemoveFromHash("route1", "c9e15763f722f23e98a29decdfae341b98d53056")
|
||||
require.NoError(err)
|
||||
require.True(removed)
|
||||
|
||||
l, err = s.ListMagnets()
|
||||
require.NoError(err)
|
||||
require.Len(l, 1)
|
||||
require.Len(l["route2"], 1)
|
||||
require.Equal(l["route2"][0], m1)
|
||||
|
||||
require.NoError(s.Close())
|
||||
require.NoError(cs.Close())
|
||||
|
||||
}
|
13
torrent/loader/loader.go
Normal file
13
torrent/loader/loader.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package loader
|
||||
|
||||
type Loader interface {
|
||||
ListMagnets() (map[string][]string, error)
|
||||
ListTorrentPaths() (map[string][]string, error)
|
||||
}
|
||||
|
||||
type LoaderAdder interface {
|
||||
Loader
|
||||
|
||||
RemoveFromHash(r, h string) (bool, error)
|
||||
AddMagnet(r, m string) error
|
||||
}
|
179
torrent/service.go
Normal file
179
torrent/service.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
package torrent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/distribyted/distribyted/fs"
|
||||
"github.com/distribyted/distribyted/torrent/loader"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
c *torrent.Client
|
||||
|
||||
s *Stats
|
||||
|
||||
mu sync.Mutex
|
||||
fss map[string]fs.Filesystem
|
||||
|
||||
cfgLoader loader.Loader
|
||||
db loader.LoaderAdder
|
||||
|
||||
log zerolog.Logger
|
||||
}
|
||||
|
||||
func NewService(cfg loader.Loader, db loader.LoaderAdder, stats *Stats, c *torrent.Client) *Service {
|
||||
l := log.Logger.With().Str("component", "torrent-service").Logger()
|
||||
return &Service{
|
||||
log: l,
|
||||
s: stats,
|
||||
c: c,
|
||||
fss: make(map[string]fs.Filesystem),
|
||||
cfgLoader: cfg,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Load() (map[string]fs.Filesystem, error) {
|
||||
// Load from config
|
||||
s.log.Info().Msg("adding torrents from configuration")
|
||||
if err := s.load(s.cfgLoader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load from DB
|
||||
s.log.Info().Msg("adding torrents from database")
|
||||
return s.fss, s.load(s.db)
|
||||
}
|
||||
|
||||
func (s *Service) load(l loader.Loader) error {
|
||||
list, err := l.ListMagnets()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for r, ms := range list {
|
||||
for _, m := range ms {
|
||||
if err := s.addMagnet(r, m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list, err = l.ListTorrentPaths()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for r, ms := range list {
|
||||
for _, p := range ms {
|
||||
if err := s.addTorrentPath(r, p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) AddMagnet(r, m string) error {
|
||||
if err := s.addMagnet(r, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add to db
|
||||
return s.db.AddMagnet(r, m)
|
||||
}
|
||||
|
||||
func (s *Service) addTorrentPath(r, p string) error {
|
||||
// Add to client
|
||||
t, err := s.c.AddTorrentFromFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.addTorrent(r, t)
|
||||
}
|
||||
|
||||
func (s *Service) addMagnet(r, m string) error {
|
||||
// Add to client
|
||||
t, err := s.c.AddMagnet(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.addTorrent(r, t)
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) addTorrent(r string, t *torrent.Torrent) error {
|
||||
// only get info if name is not available
|
||||
if t.Info() == nil {
|
||||
s.log.Info().Str("hash", t.InfoHash().String()).Msg("getting torrent info")
|
||||
<-t.GotInfo()
|
||||
}
|
||||
|
||||
// Add to stats
|
||||
s.s.Add(r, t)
|
||||
|
||||
// Add to filesystems
|
||||
folder := path.Join("/", r)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
_, ok := s.fss[folder]
|
||||
if !ok {
|
||||
s.fss[folder] = fs.NewTorrent()
|
||||
}
|
||||
|
||||
tfs, ok := s.fss[folder].(*fs.Torrent)
|
||||
if !ok {
|
||||
return errors.New("error adding torrent to filesystem")
|
||||
}
|
||||
|
||||
tfs.AddTorrent(t)
|
||||
s.log.Info().Str("name", t.Info().Name).Str("route", r).Msg("torrent added")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) RemoveFromHash(r, h string) error {
|
||||
// Remove from db
|
||||
deleted, err := s.db.RemoveFromHash(r, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !deleted {
|
||||
return fmt.Errorf("element with hash %v on route %v cannot be removed", h, r)
|
||||
}
|
||||
|
||||
// Remove from stats
|
||||
s.s.Del(r, h)
|
||||
|
||||
// Remove from fs
|
||||
folder := path.Join("/", r)
|
||||
|
||||
tfs, ok := s.fss[folder].(*fs.Torrent)
|
||||
if !ok {
|
||||
return errors.New("error removing torrent from filesystem")
|
||||
}
|
||||
|
||||
tfs.RemoveTorrent(h)
|
||||
|
||||
// Remove from client
|
||||
var mh metainfo.Hash
|
||||
if err := mh.FromHexString(h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, ok := s.c.Torrent(metainfo.NewHashFromHex(h))
|
||||
if ok {
|
||||
t.Drop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
package stats
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
|
@ -37,6 +39,12 @@ type TorrentStats struct {
|
|||
PieceSize int64 `json:"pieceSize"`
|
||||
}
|
||||
|
||||
type byName []*TorrentStats
|
||||
|
||||
func (a byName) Len() int { return len(a) }
|
||||
func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||
|
||||
type GlobalTorrentStats struct {
|
||||
DownloadedBytes int64 `json:"downloadedBytes"`
|
||||
UploadedBytes int64 `json:"uploadedBytes"`
|
||||
|
@ -54,7 +62,7 @@ func (a ByName) Len() int { return len(a) }
|
|||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||
|
||||
type stats struct {
|
||||
type stat struct {
|
||||
totalDownloadBytes int64
|
||||
downloadBytes int64
|
||||
totalUploadBytes int64
|
||||
|
@ -64,39 +72,58 @@ type stats struct {
|
|||
time time.Time
|
||||
}
|
||||
|
||||
type Torrent struct {
|
||||
type Stats struct {
|
||||
mut sync.Mutex
|
||||
torrents map[string]*torrent.Torrent
|
||||
torrentsByRoute map[string][]*torrent.Torrent
|
||||
previousStats map[string]*stats
|
||||
torrentsByRoute map[string]map[string]*torrent.Torrent
|
||||
previousStats map[string]*stat
|
||||
|
||||
gTime time.Time
|
||||
}
|
||||
|
||||
func NewTorrent() *Torrent {
|
||||
return &Torrent{
|
||||
func NewStats() *Stats {
|
||||
return &Stats{
|
||||
gTime: time.Now(),
|
||||
torrents: make(map[string]*torrent.Torrent),
|
||||
torrentsByRoute: make(map[string][]*torrent.Torrent),
|
||||
previousStats: make(map[string]*stats),
|
||||
torrentsByRoute: make(map[string]map[string]*torrent.Torrent),
|
||||
previousStats: make(map[string]*stat),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Torrent) Add(route string, t *torrent.Torrent) {
|
||||
s.torrents[t.InfoHash().String()] = t
|
||||
s.previousStats[t.InfoHash().String()] = &stats{}
|
||||
func (s *Stats) Add(route string, t *torrent.Torrent) {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
|
||||
tbr := s.torrentsByRoute[route]
|
||||
s.torrentsByRoute[route] = append(tbr, t)
|
||||
h := t.InfoHash().String()
|
||||
|
||||
s.torrents[h] = t
|
||||
s.previousStats[h] = &stat{}
|
||||
|
||||
_, ok := s.torrentsByRoute[route]
|
||||
if !ok {
|
||||
s.torrentsByRoute[route] = make(map[string]*torrent.Torrent)
|
||||
}
|
||||
|
||||
func (s *Torrent) RemoveAll() {
|
||||
// TODO lock
|
||||
s.torrents = make(map[string]*torrent.Torrent)
|
||||
s.previousStats = make(map[string]*stats)
|
||||
s.torrentsByRoute = make(map[string][]*torrent.Torrent)
|
||||
s.torrentsByRoute[route][h] = t
|
||||
}
|
||||
|
||||
func (s *Torrent) Stats(hash string) (*TorrentStats, error) {
|
||||
func (s *Stats) Del(route, hash string) {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
delete(s.torrents, hash)
|
||||
delete(s.previousStats, hash)
|
||||
ts, ok := s.torrentsByRoute[route]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
delete(ts, hash)
|
||||
}
|
||||
|
||||
func (s *Stats) Stats(hash string) (*TorrentStats, error) {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
|
||||
t, ok := s.torrents[hash]
|
||||
if !(ok) {
|
||||
return nil, ErrTorrentNotFound
|
||||
|
@ -107,7 +134,10 @@ func (s *Torrent) Stats(hash string) (*TorrentStats, error) {
|
|||
return s.stats(now, t, true), nil
|
||||
}
|
||||
|
||||
func (s *Torrent) RoutesStats() []*RouteStats {
|
||||
func (s *Stats) RoutesStats() []*RouteStats {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
var out []*RouteStats
|
||||
|
@ -117,6 +147,9 @@ func (s *Torrent) RoutesStats() []*RouteStats {
|
|||
ts := s.stats(now, t, true)
|
||||
tStats = append(tStats, ts)
|
||||
}
|
||||
|
||||
sort.Sort(byName(tStats))
|
||||
|
||||
rs := &RouteStats{
|
||||
Name: r,
|
||||
TorrentStats: tStats,
|
||||
|
@ -127,7 +160,10 @@ func (s *Torrent) RoutesStats() []*RouteStats {
|
|||
return out
|
||||
}
|
||||
|
||||
func (s *Torrent) GlobalStats() *GlobalTorrentStats {
|
||||
func (s *Stats) GlobalStats() *GlobalTorrentStats {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
var totalDownload int64
|
||||
|
@ -148,9 +184,12 @@ func (s *Torrent) GlobalStats() *GlobalTorrentStats {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Torrent) stats(now time.Time, t *torrent.Torrent, chunks bool) *TorrentStats {
|
||||
func (s *Stats) stats(now time.Time, t *torrent.Torrent, chunks bool) *TorrentStats {
|
||||
ts := &TorrentStats{}
|
||||
prev := s.previousStats[t.InfoHash().String()]
|
||||
prev, ok := s.previousStats[t.InfoHash().String()]
|
||||
if !ok {
|
||||
return &TorrentStats{}
|
||||
}
|
||||
if s.returnPreviousMeasurements(now) {
|
||||
ts.DownloadedBytes = prev.downloadBytes
|
||||
ts.UploadedBytes = prev.uploadBytes
|
||||
|
@ -158,7 +197,7 @@ func (s *Torrent) stats(now time.Time, t *torrent.Torrent, chunks bool) *Torrent
|
|||
st := t.Stats()
|
||||
rd := st.BytesReadData.Int64()
|
||||
wd := st.BytesWrittenData.Int64()
|
||||
ist := &stats{
|
||||
ist := &stat{
|
||||
downloadBytes: rd - prev.totalDownloadBytes,
|
||||
uploadBytes: wd - prev.totalUploadBytes,
|
||||
totalDownloadBytes: rd,
|
||||
|
@ -217,6 +256,6 @@ func (s *Torrent) stats(now time.Time, t *torrent.Torrent, chunks bool) *Torrent
|
|||
|
||||
const gap time.Duration = 2 * time.Second
|
||||
|
||||
func (s *Torrent) returnPreviousMeasurements(now time.Time) bool {
|
||||
func (s *Stats) returnPreviousMeasurements(now time.Time) bool {
|
||||
return now.Sub(s.gTime) < gap
|
||||
}
|
92
torrent/store.go
Normal file
92
torrent/store.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package torrent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/dht/v2/bep44"
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
dlog "github.com/distribyted/distribyted/log"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var _ bep44.Store = &FileItemStore{}
|
||||
|
||||
type FileItemStore struct {
|
||||
ttl time.Duration
|
||||
db *badger.DB
|
||||
}
|
||||
|
||||
func NewFileItemStore(path string, itemsTTL time.Duration) (*FileItemStore, error) {
|
||||
l := log.Logger.With().Str("component", "item-store").Logger()
|
||||
db, err := badger.Open(badger.DefaultOptions(path).WithLogger(&dlog.Badger{L: l}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.RunValueLogGC(0.5)
|
||||
if err != nil && err != badger.ErrNoRewrite {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FileItemStore{
|
||||
db: db,
|
||||
ttl: itemsTTL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fis *FileItemStore) Put(i *bep44.Item) error {
|
||||
tx := fis.db.NewTransaction(true)
|
||||
defer tx.Discard()
|
||||
|
||||
key := i.Target()
|
||||
var value bytes.Buffer
|
||||
|
||||
enc := gob.NewEncoder(&value)
|
||||
if err := enc.Encode(i); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := badger.NewEntry(key[:], value.Bytes()).WithTTL(fis.ttl)
|
||||
if err := tx.SetEntry(e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (fis *FileItemStore) Get(t bep44.Target) (*bep44.Item, error) {
|
||||
tx := fis.db.NewTransaction(false)
|
||||
defer tx.Discard()
|
||||
|
||||
dbi, err := tx.Get(t[:])
|
||||
if err == badger.ErrKeyNotFound {
|
||||
return nil, bep44.ErrItemNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valb, err := dbi.ValueCopy(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(valb)
|
||||
dec := gob.NewDecoder(buf)
|
||||
var i *bep44.Item
|
||||
if err := dec.Decode(&i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (fis *FileItemStore) Del(t bep44.Target) error {
|
||||
// ignore this
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fis *FileItemStore) Close() error {
|
||||
return fis.db.Close()
|
||||
}
|
Loading…
Reference in a new issue