Code cleanup

This commit is contained in:
simon987 2018-04-21 13:27:13 -04:00
parent 4eb9cf6b63
commit 87f35571fa
20 changed files with 811 additions and 856 deletions

View File

@ -8,9 +8,10 @@ default_options = {
"EbookContentLength": "2000",
"MimeGuesser": "extension", # extension, content
"CheckSumCalculators": "", # md5, sha1, sha256
"FileParsers": "media, text, picture, font" # media, text, picture
"FileParsers": "media, text, picture, font, pdf, docx, spreadsheet, ebook"
}
index_every = 10000
nGramMin = 3
nGramMax = 3
bcrypt_rounds = 14

View File

@ -12,6 +12,7 @@ from thumbnail import ThumbnailGenerator
from storage import Directory
import shutil
import config
from ctypes import c_char_p
class RunningTask:
@ -166,15 +167,13 @@ class TaskManager:
done.value = 1
def cancel_task(self):
self.current_task = None
self.current_process.terminate()
self.current_task.done.value = 1
def check_new_task(self):
if self.current_task is None:
for i in sorted(self.storage.tasks(), reverse=True):
if not self.storage.tasks()[i].completed:
self.start_task(self.storage.tasks()[i])
self.start_task(self.storage.tasks()[i])
else:
if self.current_task.done.value == 1:
@ -183,3 +182,4 @@ class TaskManager:
self.current_task = None

View File

@ -78,8 +78,14 @@ class Indexer:
self.es.indices.put_mapping(body={"properties": {
"path": {"type": "text", "analyzer": "path_analyser", "copy_to": "suggest-path"},
"suggest-path": {"type": "completion", "analyzer": "keyword"},
"mime": {"type": "text", "analyzer": "path_analyser", "copy_to": "mime_kw"},
"mime_kw": {"type": "keyword"},
"mime": {"type": "keyword"},
"encoding": {"type": "keyword"},
"format_name": {"type": "keyword"},
"format_long_name": {"type": "keyword"},
"duration": {"type": "float"},
"width": {"type": "integer"},
"height": {"type": "integer"},
"mtime": {"type": "integer"},
"directory": {"type": "short"},
"name": {"analyzer": "my_nGram", "type": "text"},
"album": {"analyzer": "my_nGram", "type": "text"},

View File

@ -181,18 +181,9 @@ class MediaFileParser(GenericFileParser):
if "format" in metadata:
if "bit_rate" in metadata["format"]:
info["bit_rate"] = int(metadata["format"]["bit_rate"])
if "nb_streams" in metadata["format"]:
info["nb_streams"] = int(metadata["format"]["nb_streams"])
if "duration" in metadata["format"]:
info["duration"] = float(metadata["format"]["duration"])
if "format_name" in metadata["format"]:
info["format_name"] = metadata["format"]["format_name"]
if "format_long_name" in metadata["format"]:
info["format_long_name"] = metadata["format"]["format_long_name"]
@ -243,10 +234,10 @@ class PictureFileParser(GenericFileParser):
with open(full_path, "rb") as image_file:
with Image.open(image_file) as image:
info["mode"] = image.mode
info["format"] = image.format
info["format_name"] = image.format
info["width"] = image.width
info["height"] = image.height
except (OSError, ValueError) as e:
except (OSError, ValueError):
pass
return info

10
run.py
View File

@ -263,13 +263,6 @@ def get_current_task():
return ""
@app.route("/task/current/cancel")
def cancel_current_task():
tm.cancel_task()
return redirect("/task")
@app.route("/task/add")
def task_add():
type = request.args.get("type")
@ -284,6 +277,9 @@ def task_add():
def task_del(task_id):
storage.del_task(task_id)
if tm.current_task is not None and task_id == tm.current_task.task.id:
tm.cancel_task()
return redirect("/task")

View File

@ -57,7 +57,7 @@ class Search:
"aggs": {
"mimeTypes": {
"terms": {
"field": "mime_kw",
"field": "mime",
"size": 10000
}
}

View File

@ -10,7 +10,6 @@ class MediaFileParserTest(TestCase):
info = parser.parse("test_files/cat1.wav")
self.assertEqual(info["bit_rate"], 256044)
self.assertEqual(info["format_name"], "wav")
self.assertEqual(info["format_long_name"], "WAV / WAVE (Waveform Audio)")
self.assertEqual(info["duration"], 20.173875)
@ -20,7 +19,6 @@ class MediaFileParserTest(TestCase):
info = parser.parse("test_files/vid1.mp4")
self.assertEqual(info["bit_rate"], 513012)
self.assertEqual(info["format_name"], "mov,mp4,m4a,3gp,3g2,mj2")
self.assertEqual(info["format_long_name"], "QuickTime / MOV")
self.assertEqual(info["duration"], 5.334)
@ -30,7 +28,6 @@ class MediaFileParserTest(TestCase):
info = parser.parse("test_files/vid2.webm")
self.assertEqual(info["bit_rate"], 343153)
self.assertEqual(info["format_name"], "matroska,webm")
self.assertEqual(info["format_long_name"], "Matroska / WebM")
self.assertEqual(info["duration"], 10.619)
@ -40,7 +37,6 @@ class MediaFileParserTest(TestCase):
info = parser.parse("test_files/vid3.ogv")
self.assertEqual(info["bit_rate"], 590261)
self.assertEqual(info["format_name"], "ogg")
self.assertEqual(info["format_long_name"], "Ogg")
self.assertEqual(info["duration"], 10.618867)

View File

@ -13,7 +13,7 @@ class PictureFileParserTest(TestCase):
self.assertEqual(info["mode"], "RGB")
self.assertEqual(info["width"], 420)
self.assertEqual(info["height"], 315)
self.assertEqual(info["format"], "JPEG")
self.assertEqual(info["format_name"], "JPEG")
def test_parse_png(self):
@ -24,7 +24,7 @@ class PictureFileParserTest(TestCase):
self.assertEqual(info["mode"], "RGBA")
self.assertEqual(info["width"], 288)
self.assertEqual(info["height"], 64)
self.assertEqual(info["format"], "PNG")
self.assertEqual(info["format_name"], "PNG")
def test_parse_gif(self):
@ -35,7 +35,7 @@ class PictureFileParserTest(TestCase):
self.assertEqual(info["mode"], "P")
self.assertEqual(info["width"], 420)
self.assertEqual(info["height"], 315)
self.assertEqual(info["format"], "GIF")
self.assertEqual(info["format_name"], "GIF")
def test_parse_bmp(self):
@ -46,4 +46,4 @@ class PictureFileParserTest(TestCase):
self.assertEqual(info["mode"], "RGB")
self.assertEqual(info["width"], 150)
self.assertEqual(info["height"], 200)
self.assertEqual(info["format"], "BMP")
self.assertEqual(info["format_name"], "BMP")

5
static/css/fa-brands.min.css vendored Normal file
View File

@ -0,0 +1,5 @@
/*!
* Font Awesome Free 5.0.8 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face{font-family:Font Awesome\ 5 Brands;font-style:normal;font-weight:400;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:Font Awesome\ 5 Brands}

5
static/css/fa-regular.min.css vendored Normal file
View File

@ -0,0 +1,5 @@
/*!
* Font Awesome Free 5.0.8 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:Font Awesome\ 5 Free;font-weight:400}

5
static/css/fa-solid.min.css vendored Normal file
View File

@ -0,0 +1,5 @@
/*!
* Font Awesome Free 5.0.8 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
*/
@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:Font Awesome\ 5 Free;font-weight:900}

5
static/css/fontawesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

147
static/css/search.css Normal file
View File

@ -0,0 +1,147 @@
body {overflow-y:scroll;}
.document {
padding: 0.5rem;
}
.document p {
margin-bottom: 0;
}
.document:hover p {
text-decoration: underline;
}
.badge-video {
color: #FFFFFF;
background-color: #F27761;
}
.badge-image {
color: #FFFFFF;
background-color: #AA99C9;
}
.badge-audio {
color: #FFFFFF;
background-color: #00ADEF;
}
.badge-resolution {
color: #212529;
background-color: #FFC107;
}
.badge-text {
color: #FFFFFF;
background-color: #FAAB3C;
}
.card-img-top {
display: block;
min-width: 64px;
max-width: 100%;
max-height: 256px;
width: unset;
margin: 0 auto 0;
padding: 3px 3px 0 3px;
}
.card-img-overlay {
pointer-events: none;
padding: 0.75rem;
bottom: 0;
top: unset;
left: unset;
right: unset;
}
.file-title {
font-size: 10pt;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.badge {
margin-right: 3px;
}
.fit {
width: 100%;
height: 100%;
padding: 3px;
min-width: 64px;
max-width: 100%;
max-height: 256px;
}
.audio-fit {
height: 39px;
vertical-align: bottom;
}
@media (min-width: 1200px) {
.card-columns {
column-count: 4;
}
}
@media (min-width: 1500px) {
.container {
max-width: 1440px;
}
.card-columns {
column-count: 5;
}
}
.hl {
background: #fff217;
}
.content-div {
font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
font-size: 13px;
padding: 1em;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
margin: 3px;
}
.irs-single, .irs-from, .irs-to {
font-size: 13px;
}
.irs-slider {
cursor: col-resize;
}
.custom-select {
overflow: auto;
}
.irs {
margin-top: 1em;
margin-bottom: 1em;
}
.inspire-tree .selected > .wholerow, .inspire-tree .selected > .title-wrap:hover + .wholerow
{
background: none;
}
.inspire-tree {
font-weight: 400;
font-size: 14px;
font-family: Helvetica, Nueue, Verdana, sans-serif;
max-height: 450px;
overflow: auto;
}
.btn-xs {
padding: .1rem .3rem;
font-size: .875rem;
border-radius: .2rem;
}

604
static/js/search.js Normal file
View File

@ -0,0 +1,604 @@
let searchBar = document.getElementById("searchBar");
let pathBar = document.getElementById("pathBar");
let must_match = true;
let scroll_id = null;
let docCount = 0;
let searchQueued = false;
let coolingDown = false;
let selectedDirs = [];
function toggleSearchBar() {
must_match = !must_match;
searchQueued = true;
}
let tree = new InspireTree({
selection: {
mode: 'checkbox'
},
data: mimeMap
});
new InspireTreeDOM(tree, {
target: '.tree'
});
//Select all
tree.select();
tree.node("any").deselect();
tree.on("node.click", function(event, node, handler) {
event.preventTreeDefault();
if (node.id === "any") {
if (!node.itree.state.checked) {
tree.deselect();
}
} else {
tree.node("any").deselect();
}
handler();
searchQueued = true;
});
new autoComplete({
selector: '#pathBar',
minChars: 1,
delay: 75,
renderItem: function (item){
return '<div class="autocomplete-suggestion" data-val="' + item + '">' + item + '</div>';
},
source: async function(term, suggest) {
term = term.toLowerCase();
const choices = await getPathChoices();
let matches = [];
for (let i=0; i<choices.length; i++) {
if (~choices[i].toLowerCase().indexOf(term)) {
matches.push(choices[i]);
}
}
suggest(matches);
},
onSelect: function() {
searchQueued = true;
}
});
function makeStatsCard(searchResult) {
let statsCard = document.createElement("div");
statsCard.setAttribute("class", "card");
let statsCardBody = document.createElement("div");
statsCardBody.setAttribute("class", "card-body");
let stat = document.createElement("p");
stat.appendChild(document.createTextNode(searchResult["hits"]["total"] + " results in " + searchResult["took"] + "ms"));
let sizeStat = document.createElement("span");
sizeStat.appendChild(document.createTextNode(humanFileSize(searchResult["aggregations"]["total_size"]["value"])));
statsCardBody.appendChild(stat);
statsCardBody.appendChild(sizeStat);
statsCard.appendChild(statsCardBody);
return statsCard;
}
function makeResultContainer() {
let resultContainer = document.createElement("div");
resultContainer.setAttribute("class", "card-columns");
return resultContainer;
}
/**
* https://stackoverflow.com/questions/10420352
*/
function humanFileSize(bytes) {
if(bytes === 0) {
return "? B"
}
let thresh = 1000;
if(Math.abs(bytes) < thresh) {
return bytes + ' B';
}
let units = ['kB','MB','GB','TB','PB','EB','ZB','YB'];
let u = -1;
do {
bytes /= thresh;
++u;
} while(Math.abs(bytes) >= thresh && u < units.length - 1);
return bytes.toFixed(1) + ' ' + units[u];
}
function initPopover() {
$('[data-toggle="popover"]').popover({
trigger: "focus",
delay: { "show": 0, "hide": 100 },
placement: "bottom",
html: true
});
}
/**
* Enable gif loading on hover
* @param thumbnail
* @param documentId
*/
function gifOver(thumbnail, documentId) {
let callee = arguments.callee;
thumbnail.addEventListener("mouseover", function () {
thumbnail.mouseStayedOver = true;
window.setTimeout(function() {
if (thumbnail.mouseStayedOver) {
thumbnail.removeEventListener('mouseover', callee, false);
//Load gif
thumbnail.setAttribute("src", "/file/" + documentId);
}
}, 750); //todo grab hover time from config
});
thumbnail.addEventListener("mouseout", function() {
//Reset timer
thumbnail.mouseStayedOver = false;
thumbnail.setAttribute("src", "/thumb/" + documentId);
})
}
function videoOver(thumbnail, imgWrapper, thumbnailOverlay, documentId, docCard) {
docCard.addEventListener("focus", function () {
let callee = arguments.callee;
docCard.mouseStayedOver = true;
window.setTimeout(function() {
if(docCard.mouseStayedOver) {
docCard.removeEventListener('focus', callee, false);
imgWrapper.removeChild(thumbnail);
imgWrapper.removeChild(thumbnailOverlay);
let video = document.createElement("video");
let vidSource = document.createElement("source");
vidSource.setAttribute("src", "/file/" + documentId);
vidSource.setAttribute("type", "video/webm");
video.appendChild(vidSource);
video.setAttribute("class", "fit");
video.setAttribute("loop", "");
video.setAttribute("controls", "");
video.setAttribute("preload", "");
video.setAttribute("poster", "/thumb/" + documentId);
imgWrapper.appendChild(video);
video.addEventListener("dblclick", function() {
video.webkitRequestFullScreen();
})
}
}, 750);
});
docCard.addEventListener("blur", function() {
docCard.mouseStayedOver = false;
});
}
function downloadPopover(element, documentId) {
element.setAttribute("data-content",
'<a class="btn btn-sm btn-primary" href="/dl/'+ documentId +'"><i class="fas fa-download"></i> Download</a>' +
'<a class="btn btn-sm btn-success" style="margin-left:3px;" href="/file/'+ documentId + '" target="_blank"><i class="fas fa-eye"></i> View</a>');
element.setAttribute("data-toggle", "popover");
element.addEventListener("mouseover", function() {
element.focus();
});
}
/**
*
* @param hit
* @returns {Element}
*/
function createDocCard(hit) {
let docCard = document.createElement("div");
docCard.setAttribute("class", "card");
docCard.setAttribute("tabindex", "-1");
let docCardBody = document.createElement("div");
docCardBody.setAttribute("class", "card-body document");
let link = document.createElement("a");
link.setAttribute("href", "/document/" + hit["_id"]);
link.setAttribute("target", "_blank");
//Title
let title = document.createElement("p");
title.setAttribute("class", "file-title");
let extension = hit["_source"].hasOwnProperty("extension") && hit["_source"]["extension"] !== "" ? "." + hit["_source"]["extension"] : "";
if (hit.hasOwnProperty("highlight") && hit["highlight"].hasOwnProperty("name")) {
title.insertAdjacentHTML('afterbegin', hit["highlight"]["name"] + extension);
} else {
title.appendChild(document.createTextNode(hit["_source"]["name"] + extension));
}
title.setAttribute("title", hit["_source"]["path"] + hit["_source"]["name"] + extension);
docCard.appendChild(title);
let tagContainer = document.createElement("div");
tagContainer.setAttribute("class", "card-text");
if (hit["_source"].hasOwnProperty("mime") && hit["_source"]["mime"] !== null) {
let tags = [];
let thumbnail = null;
let thumbnailOverlay = null;
let imgWrapper = document.createElement("div");
imgWrapper.setAttribute("style", "position: relative");
let mimeCategory = hit["_source"]["mime"].split("/")[0];
//Thumbnail
switch (mimeCategory) {
case "video":
case "image":
thumbnail = document.createElement("img");
thumbnail.setAttribute("class", "card-img-top");
thumbnail.setAttribute("src", "/thumb/" + hit["_id"]);
break;
}
//Thumbnail overlay
switch (mimeCategory) {
case "image":
thumbnailOverlay = document.createElement("div");
thumbnailOverlay.setAttribute("class", "card-img-overlay");
//Resolution
let resolutionBadge = document.createElement("span");
resolutionBadge.setAttribute("class", "badge badge-resolution");
if (hit["_source"].hasOwnProperty("width")) {
resolutionBadge.appendChild(document.createTextNode(hit["_source"]["width"] + "x" + hit["_source"]["height"]));
}
thumbnailOverlay.appendChild(resolutionBadge);
var format = hit["_source"]["format"];
//Hover
if(format === "GIF") {
gifOver(thumbnail, hit["_id"]);
}
break;
case "video":
thumbnailOverlay = document.createElement("div");
thumbnailOverlay.setAttribute("class", "card-img-overlay");
//Duration
let durationBadge = document.createElement("span");
durationBadge.setAttribute("class", "badge badge-resolution");
durationBadge.appendChild(document.createTextNode(parseFloat(hit["_source"]["duration"]).toFixed(2) + "s"));
thumbnailOverlay.appendChild(durationBadge);
//Hover
videoOver(thumbnail, imgWrapper, thumbnailOverlay, hit["_id"], docCard)
}
//Tags
switch (mimeCategory) {
case "video":
if (hit["_source"].hasOwnProperty("format_long_name")) {
let formatTag = document.createElement("span");
formatTag.setAttribute("class", "badge badge-pill badge-video");
formatTag.appendChild(document.createTextNode(hit["_source"]["format_long_name"].replace(" ", "")));
tags.push(formatTag);
}
break;
case "image": {
let formatTag = document.createElement("span");
formatTag.setAttribute("class", "badge badge-pill badge-image");
formatTag.appendChild(document.createTextNode(format));
tags.push(formatTag);
}
break;
case "audio": {
if (hit["_source"].hasOwnProperty("format_long_name")) {
let formatTag = document.createElement("span");
formatTag.setAttribute("class", "badge badge-pill badge-audio");
formatTag.appendChild(document.createTextNode(hit["_source"]["format_long_name"]));
tags.push(formatTag);
}
}
break;
case "text": {
let formatTag = document.createElement("span");
formatTag.setAttribute("class", "badge badge-pill badge-text");
formatTag.appendChild(document.createTextNode(hit["_source"]["encoding"]));
tags.push(formatTag);
}
break;
}
//Content
if (hit.hasOwnProperty("highlight") && hit["highlight"].hasOwnProperty("content")) {
let contentDiv = document.createElement("div");
contentDiv.setAttribute("class", "content-div bg-light");
contentDiv.insertAdjacentHTML('afterbegin', hit["highlight"]["content"][0]);
docCard.appendChild(contentDiv);
}
//Audio
if (mimeCategory === "audio" && hit["_source"].hasOwnProperty("format_long_name")) {
let audio = document.createElement("audio");
audio.setAttribute("preload", "none");
audio.setAttribute("class", "audio-fit fit");
audio.setAttribute("controls", "");
audio.setAttribute("type", hit["_source"]["mime"]);
audio.setAttribute("src", "file/" + hit["_id"]);
docCard.appendChild(audio)
}
if (thumbnail !== null) {
imgWrapper.appendChild(thumbnail);
docCard.appendChild(imgWrapper);
}
if (thumbnailOverlay !== null) {
imgWrapper.appendChild(thumbnailOverlay);
}
for (let i = 0; i < tags.length; i++) {
tagContainer.appendChild(tags[i]);
}
}
//Size tag
let sizeTag = document.createElement("small");
sizeTag.appendChild(document.createTextNode(humanFileSize(hit["_source"]["size"])));
sizeTag.setAttribute("class", "text-muted");
tagContainer.appendChild(sizeTag);
//Download button
downloadPopover(docCard, hit["_id"]);
docCardBody.appendChild(link);
docCard.appendChild(docCardBody);
link.appendChild(title);
docCardBody.appendChild(tagContainer);
return docCard;
}
function makePageIndicator(searchResult) {
let pageIndicator = document.createElement("div");
pageIndicator.appendChild(document.createTextNode(docCount + " / " +searchResult["hits"]["total"]));
return pageIndicator;
}
function insertHits(resultContainer, hits) {
for (let i = 0 ; i < hits.length; i++) {
resultContainer.appendChild(createDocCard(hits[i]));
docCount++;
}
}
window.addEventListener("scroll", function () {
if (!coolingDown) {
let threshold = 350;
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - threshold) {
//load next page
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
let searchResult = JSON.parse(this.responseText);
let searchResults = document.getElementById("searchResults");
let hits = searchResult["hits"]["hits"];
//Page indicator
let pageIndicator = makePageIndicator(searchResult);
searchResults.appendChild(pageIndicator);
//Result container
let resultContainer = makeResultContainer();
searchResults.appendChild(resultContainer);
insertHits(resultContainer, hits);
initPopover();
if (hits.length !== 0) {
coolingDown = false;
}
}
};
xhttp.open("GET", "/scroll?scroll_id=" + scroll_id, true);
xhttp.send();
coolingDown = true;
}
}
});
function getSelectedMimeTypes() {
let mimeTypes = [];
let selected = tree.selected();
for (let i = 0; i < selected.length; i++) {
if(selected[i].id === "any") {
return "any"
}
//Only get children
if (selected[i].text.indexOf("(") !== -1) {
mimeTypes.push(selected[i].id);
}
}
return mimeTypes
}
function search() {
if (searchQueued === true) {
searchQueued = false;
//Clear old search results
let searchResults = document.getElementById("searchResults");
while (searchResults.firstChild) {
searchResults.removeChild(searchResults.firstChild);
}
let query = searchBar.value;
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
let searchResult = JSON.parse(this.responseText);
scroll_id = searchResult["_scroll_id"];
//Search stats
searchResults.appendChild(makeStatsCard(searchResult));
//Autocomplete
if (searchResult.hasOwnProperty("suggest") && searchResult["suggest"].hasOwnProperty("path")) {
pathAutoComplete = [];
for (let i = 0; i < searchResult["suggest"]["path"][0]["options"].length; i++) {
pathAutoComplete.push(searchResult["suggest"]["path"][0]["options"][i].text)
}
}
//Setup page
let resultContainer = makeResultContainer();
searchResults.appendChild(resultContainer);
//Insert search results (hits)
docCount = 0;
insertHits(resultContainer, searchResult["hits"]["hits"]);
//Initialise download/view button popover
initPopover();
}
};
xhttp.open("POST", "/search", true);
let postBody = {};
postBody.q = query;
postBody.size_min = size_min;
postBody.size_max = size_max;
postBody.mime_types = getSelectedMimeTypes();
postBody.must_match = must_match;
postBody.directories = selectedDirs;
postBody.path = pathBar.value.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
xhttp.setRequestHeader('content-type', 'application/json');
xhttp.send(JSON.stringify(postBody));
}
}
let pathAutoComplete = [];
let size_min = 0;
let size_max = 10000000000000;
searchBar.addEventListener("keyup", function () {
searchQueued = true;
});
//Size slider
let sizeSlider = $("#sizeSlider").ionRangeSlider({
type: "double",
grid: false,
force_edges: true,
min: 0,
max: 3684.03149864,
from: 0,
to: 3684.03149864,
min_interval: 5,
drag_interval: true,
prettify: function (num) {
if(num === 0) {
return "0 B"
} else if (num >= 3684) {
return humanFileSize(num * num * num) + "+";
}
return humanFileSize(num * num * num)
},
onChange: function(e) {
size_min = (e.from * e.from * e.from);
size_max = (e.to * e.to * e.to);
if (e.to >= 3684) {
size_max = 10000000000000;
}
searchQueued = true;
}
})[0];
//Directories select
function updateDirectories() {
let selected = $('#directories').find('option:selected');
selectedDirs = [];
$(selected).each(function(){
selectedDirs.push(parseInt($(this).val()));
});
searchQueued = true;
}
document.getElementById("directories").addEventListener("change", updateDirectories);
updateDirectories();
searchQueued = false;
//Suggest
function getPathChoices() {
return new Promise(getPaths => {
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
getPaths(JSON.parse(xhttp.responseText))
}
};
xhttp.open("GET", "/suggest?prefix=" + pathBar.value, true);
xhttp.send();
});
}
window.setInterval(search, 75);

View File

@ -8,7 +8,7 @@ import config
class CheckSumCalculator:
def checksum(self, string: str):
return flask_bcrypt.generate_password_hash(string, 14) # todo load from config
return flask_bcrypt.generate_password_hash(string, config.bcrypt_rounds)
class DuplicateDirectoryException(Exception):

View File

@ -11,20 +11,18 @@
<script src="/static/js/Chart.min.js" type="text/javascript"></script>
<script src="/static/js/auto-complete.min.js" type="text/javascript"></script>
<script src="/static/js/ion.rangeSlider.min.js" type="text/javascript"></script>
<script src="/static/js/lodash.min.js" type="text/javascript"></script>
<script src="/static/js/inspire-tree.min.js" type="text/javascript"></script>
<script src="/static/js/inspire-tree-dom.min.js" type="text/javascript"></script>
{# <link href="/static/css/normalize.css" rel="stylesheet" type="text/css">#}
<link href="/static/css/fontawesome-all.min.css" rel="stylesheet" type="text/css">
<link href="/static/css/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="/static/css/auto-complete.css" rel="stylesheet" type="text/css">
<link href="/static/css/ion.rangeSlider.css" rel="stylesheet" type="text/css">
<link href="/static/css/ion.rangeSlider.skinFlat.css" rel="stylesheet" type="text/css">
<link href="/static/css/inspire-tree-light.css" rel="stylesheet" type="text/css">
{% block imports %}{% endblock %}
<style>
.info-table {

View File

@ -4,161 +4,13 @@
{% block title %}Search{% endblock title %}
{% block imports %}
<link href="/static/css/search.css" rel="stylesheet" type="text/css">
{% endblock %}
{% block body %}
<style>
body {overflow-y:scroll;}
.document {
padding: 0.5rem;
}
.document p {
margin-bottom: 0;
}
.document:hover p {
text-decoration: underline;
}
.badge-video {
color: #FFFFFF;
background-color: #F27761;
}
.badge-image {
color: #FFFFFF;
background-color: #AA99C9;
}
.badge-audio {
color: #FFFFFF;
background-color: #00ADEF;
}
.badge-resolution {
color: #212529;
background-color: #FFC107;
}
.badge-text {
color: #FFFFFF;
background-color: #FAAB3C;
}
.card-img-top {
display: block;
min-width: 64px;
max-width: 100%;
max-height: 256px;
width: unset;
margin: 0 auto 0;
padding: 3px 3px 0 3px;
}
.card-img-overlay {
pointer-events: none;
padding: 0.75rem;
bottom: 0;
top: unset;
left: unset;
right: unset;
}
.file-title {
font-size: 10pt;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.badge {
margin-right: 3px;
}
.fit {
width: 100%;
height: 100%;
padding: 3px;
min-width: 64px;
max-width: 100%;
max-height: 256px;
}
.audio-fit {
height: 39px;
vertical-align: bottom;
}
@media (min-width: 1200px) {
.card-columns {
column-count: 4;
}
}
@media (min-width: 1500px) {
.container {
max-width: 1440px;
}
.card-columns {
column-count: 5;
}
}
.hl {
color: red;
}
.content-div {
font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
font-size: 13px;
padding: 1em;
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
margin: 3px;
}
.irs-single, .irs-from, .irs-to {
font-size: 13px;
}
.irs-slider {
cursor: col-resize;
}
.custom-select {
overflow: auto;
}
.irs {
margin-top: 1em;
margin-bottom: 1em;
}
.inspire-tree .selected > .wholerow, .inspire-tree .selected > .title-wrap:hover + .wholerow
{
background: none;
}
.inspire-tree {
font-weight: 400;
font-size: 14px;
font-family: Helvetica, Nueue, Verdana, sans-serif;
max-height: 450px;
overflow: auto;
}
.btn-xs {
padding: .1rem .3rem;
font-size: .875rem;
border-radius: .2rem;
}
</style>
<div class="container">
<div class="card">
<div class="card-body">
<div class="form-group">
@ -168,14 +20,14 @@
<div class="input-group-prepend">
<div class="input-group-text">
<span onclick="document.getElementById('barToggle').click()">Must match&nbsp</span>
<input type="checkbox" id="barToggle" onclick="toggleSearchBar()" checked>
<input title="Toggle between 'Should' and 'Must' match mode" type="checkbox" id="barToggle" onclick="toggleSearchBar()" checked>
</div>
</div>
<input id="searchBar" type="search" class="form-control" placeholder="Search">
</div>
<input id="sizeSlider" name="size">
<input title="File size" id="sizeSlider" name="size">
<div class="row">
<div class="col">
@ -196,642 +48,15 @@
<div class="tree"></div>
</div>
</div>
<script type="text/javascript">
let tree = new InspireTree({
selection: {
mode: 'checkbox'
},
data: {{ mime_map | tojson }}
});
new InspireTreeDOM(tree, {
target: '.tree'
});
//Select all
tree.select();
tree.node("any").deselect();
tree.on("node.click", function(event, node, handler) {
event.preventTreeDefault();
if (node.id === "any") {
if (!node.itree.state.checked) {
tree.deselect();
}
} else {
tree.node("any").deselect();
}
handler();
searchQueued = true;
})
</script>
</div>
</div>
<script>
new autoComplete({
selector: '#pathBar',
minChars: 1,
delay: 75,
renderItem: function (item, search){
return '<div class="autocomplete-suggestion" data-val="' + item + '">' + item + '</div>';
},
source: async function(term, suggest) {
term = term.toLowerCase();
const choices = await getPathChoices();
let matches = [];
for (let i=0; i<choices.length; i++) {
if (~choices[i].toLowerCase().indexOf(term)) {
matches.push(choices[i]);
}
}
suggest(matches);
},
onSelect: function(event, term, item) {
searchQueued = true;
}
});
</script>
<div id="searchResults">
</div>
<div id="searchResults"></div>
<script>
let searchBar = document.getElementById("searchBar");
let pathBar = document.getElementById("pathBar");
let must_match = true;
let scroll_id = null;
let docCount = 0;
let treeSelected = false;
let searchQueued = false;
let coolingDown = false;
let selectedDirs = [];
function toggleSearchBar() {
must_match = !must_match;
searchQueued = true;
}
function toggleTree() {
if (treeSelected) {
tree.select();
} else {
tree.deselect();
}
treeSelected = !treeSelected;
searchQueued = true;
}
function makeStatsCard(searchResult) {
let statsCard = document.createElement("div");
statsCard.setAttribute("class", "card");
let statsCardBody = document.createElement("div");
statsCardBody.setAttribute("class", "card-body");
let stat = document.createElement("p");
stat.appendChild(document.createTextNode(searchResult["hits"]["total"] + " results in " + searchResult["took"] + "ms"));
let sizeStat = document.createElement("span");
sizeStat.appendChild(document.createTextNode(humanFileSize(searchResult["aggregations"]["total_size"]["value"])));
statsCardBody.appendChild(stat);
statsCardBody.appendChild(sizeStat);
statsCard.appendChild(statsCardBody);
return statsCard;
}
function makeResultContainer() {
let resultContainer = document.createElement("div");
resultContainer.setAttribute("class", "card-columns");
return resultContainer;
}
/**
* https://stackoverflow.com/questions/10420352
*/
function humanFileSize(bytes) {
if(bytes === 0) {
return "? B"
}
let thresh = 1000;
if(Math.abs(bytes) < thresh) {
return bytes + ' B';
}
let units = ['kB','MB','GB','TB','PB','EB','ZB','YB'];
let u = -1;
do {
bytes /= thresh;
++u;
} while(Math.abs(bytes) >= thresh && u < units.length - 1);
return bytes.toFixed(1) + ' ' + units[u];
}
function initPopover() {
$('[data-toggle="popover"]').popover({
trigger: "focus",
delay: { "show": 0, "hide": 100 },
placement: "bottom",
html: true
});
}
/**
* Enable gif loading on hover
* @param thumbnail
* @param documentId
*/
function gifOver(thumbnail, documentId) {
let callee = arguments.callee;
thumbnail.addEventListener("mouseover", function () {
thumbnail.mouseStayedOver = true;
window.setTimeout(function() {
if (thumbnail.mouseStayedOver) {
thumbnail.removeEventListener('mouseover', callee, false);
//Load gif
thumbnail.setAttribute("src", "/file/" + documentId);
}
}, 750); //todo grab hover time from config
});
thumbnail.addEventListener("mouseout", function() {
//Reset timer
thumbnail.mouseStayedOver = false;
thumbnail.setAttribute("src", "/thumb/" + documentId);
})
}
function videoOver(thumbnail, imgWrapper, thumbnailOverlay, documentId, docCard) {
docCard.addEventListener("focus", function () {
let callee = arguments.callee;
docCard.mouseStayedOver = true;
window.setTimeout(function() {
if(docCard.mouseStayedOver) {
docCard.removeEventListener('focus', callee, false);
imgWrapper.removeChild(thumbnail);
imgWrapper.removeChild(thumbnailOverlay);
let video = document.createElement("video");
let vidSource = document.createElement("source");
vidSource.setAttribute("src", "/file/" + documentId);
vidSource.setAttribute("type", "video/webm");
video.appendChild(vidSource);
video.setAttribute("class", "fit");
video.setAttribute("loop", "");
video.setAttribute("controls", "");
video.setAttribute("preload", "");
video.setAttribute("poster", "/thumb/" + documentId);
imgWrapper.appendChild(video);
docCard.addEventListener("blur", function() {
video.pause();
});
video.addEventListener("dblclick", function() {
video.webkitRequestFullScreen();
})
}
}, 750);
});
docCard.addEventListener("blur", function() {
docCard.mouseStayedOver = false;
});
}
function downloadPopover(element, documentId) {
element.setAttribute("data-content",
'<a class="btn btn-sm btn-primary" href="/dl/'+ documentId +'"><i class="fas fa-download"></i> Download</a>' +
'<a class="btn btn-sm btn-success" style="margin-left:3px;" href="/file/'+ documentId + '" target="_blank"><i class="fas fa-eye"></i> View</a>');
element.setAttribute("data-toggle", "popover");
element.addEventListener("mouseover", function() {
element.focus();
});
}
let counter = 0;
/**
*
* @param hit
* @returns {Element}
*/
function createDocCard(hit) {
let docCard = document.createElement("div");
docCard.setAttribute("class", "card");
docCard.setAttribute("tabindex", "-1");
let docCardBody = document.createElement("div");
docCardBody.setAttribute("class", "card-body document");
let link = document.createElement("a");
link.setAttribute("href", "/document/" + hit["_id"]);
link.setAttribute("target", "_blank");
//Title
let title = document.createElement("p");
title.setAttribute("class", "file-title");
let extension = hit["_source"].hasOwnProperty("extension") && hit["_source"]["extension"] !== "" ? "." + hit["_source"]["extension"] : "";
if (hit.hasOwnProperty("highlight") && hit["highlight"].hasOwnProperty("name")) {
title.insertAdjacentHTML('afterbegin', hit["highlight"]["name"] + extension);
} else {
title.appendChild(document.createTextNode(hit["_source"]["name"] + extension));
}
title.setAttribute("title", hit["_source"]["path"] + hit["_source"]["name"] + extension);
docCard.appendChild(title);
let tagContainer = document.createElement("div");
tagContainer.setAttribute("class", "card-text");
if (hit["_source"].hasOwnProperty("mime") && hit["_source"]["mime"] !== null) {
let tags = [];
let thumbnail = null;
let thumbnailOverlay = null;
let imgWrapper = document.createElement("div");
imgWrapper.setAttribute("style", "position: relative");
let mimeCategory = hit["_source"]["mime"].split("/")[0];
//Thumbnail
switch (mimeCategory) {
case "video":
case "image":
thumbnail = document.createElement("img");
thumbnail.setAttribute("class", "card-img-top");
thumbnail.setAttribute("src", "/thumb/" + hit["_id"]);
break;
}
//Thumbnail overlay
switch (mimeCategory) {
case "image":
thumbnailOverlay = document.createElement("div");
thumbnailOverlay.setAttribute("class", "card-img-overlay");
//Resolution
let resolutionBadge = document.createElement("span");
resolutionBadge.setAttribute("class", "badge badge-resolution");
if (hit["_source"].hasOwnProperty("width")) {
resolutionBadge.appendChild(document.createTextNode(hit["_source"]["width"] + "x" + hit["_source"]["height"]));
}
thumbnailOverlay.appendChild(resolutionBadge);
var format = hit["_source"]["format"];
//Hover
if(format === "GIF") {
gifOver(thumbnail, hit["_id"]);
}
break;
case "video":
thumbnailOverlay = document.createElement("div");
thumbnailOverlay.setAttribute("class", "card-img-overlay");
//Duration
let durationBadge = document.createElement("span");
durationBadge.setAttribute("class", "badge badge-resolution");
durationBadge.appendChild(document.createTextNode(parseFloat(hit["_source"]["duration"]).toFixed(2) + "s"));
thumbnailOverlay.appendChild(durationBadge);
//Hover
videoOver(thumbnail, imgWrapper, thumbnailOverlay, hit["_id"], docCard)
}
//Tags
switch (mimeCategory) {
case "video":
if (hit["_source"].hasOwnProperty("format_long_name")) {
let formatTag = document.createElement("span");
formatTag.setAttribute("class", "badge badge-pill badge-video");
formatTag.appendChild(document.createTextNode(hit["_source"]["format_long_name"].replace(" ", "")));
tags.push(formatTag);
}
break;
case "image": {
let formatTag = document.createElement("span");
formatTag.setAttribute("class", "badge badge-pill badge-image");
formatTag.appendChild(document.createTextNode(format));
tags.push(formatTag);
}
break;
case "audio": {
let formatTag = document.createElement("span");
formatTag.setAttribute("class", "badge badge-pill badge-audio");
formatTag.appendChild(document.createTextNode(hit["_source"]["format_name"]));
tags.push(formatTag);
}
break;
case "text": {
let formatTag = document.createElement("span");
formatTag.setAttribute("class", "badge badge-pill badge-text");
formatTag.appendChild(document.createTextNode(hit["_source"]["encoding"]));
tags.push(formatTag);
}
break;
}
//Content
if (hit.hasOwnProperty("highlight") && hit["highlight"].hasOwnProperty("content")) {
let contentDiv = document.createElement("div");
contentDiv.setAttribute("class", "content-div bg-light");
contentDiv.insertAdjacentHTML('afterbegin', hit["highlight"]["content"][0]);
docCard.appendChild(contentDiv);
}
//Audio
if (mimeCategory === "audio" && hit["_source"].hasOwnProperty("format_long_name")) {
let audio = document.createElement("audio");
audio.setAttribute("preload", "none");
audio.setAttribute("class", "audio-fit fit");
audio.setAttribute("controls", "");
audio.setAttribute("type", hit["_source"]["mime"]);
audio.setAttribute("src", "file/" + hit["_id"]);
docCard.appendChild(audio)
}
if (thumbnail !== null) {
imgWrapper.appendChild(thumbnail);
docCard.appendChild(imgWrapper);
}
if (thumbnailOverlay !== null) {
imgWrapper.appendChild(thumbnailOverlay);
}
for (let i = 0; i < tags.length; i++) {
tagContainer.appendChild(tags[i]);
}
}
//Size tag
let sizeTag = document.createElement("small");
sizeTag.appendChild(document.createTextNode(humanFileSize(hit["_source"]["size"])));
sizeTag.setAttribute("class", "text-muted");
tagContainer.appendChild(sizeTag);
//Download button
downloadPopover(docCard, hit["_id"]);
docCardBody.appendChild(link);
docCard.appendChild(docCardBody);
link.appendChild(title);
docCardBody.appendChild(tagContainer);
return docCard;
}
function makePageIndicator(searchResult) {
let pageIndicator = document.createElement("div");
pageIndicator.appendChild(document.createTextNode(docCount + " / " +searchResult["hits"]["total"]));
return pageIndicator;
}
function insertHits(resultContainer, hits) {
for (let i = 0 ; i < hits.length; i++) {
resultContainer.appendChild(createDocCard(hits[i]));
docCount++;
}
}
window.addEventListener("scroll", function () {
if (!coolingDown) {
let threshold = 350;
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - threshold) {
//load next page
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
let searchResult = JSON.parse(this.responseText);
let searchResults = document.getElementById("searchResults");
let hits = searchResult["hits"]["hits"];
//Page indicator
let pageIndicator = makePageIndicator(searchResult);
searchResults.appendChild(pageIndicator);
//Result container
let resultContainer = makeResultContainer();
searchResults.appendChild(resultContainer);
insertHits(resultContainer, hits);
initPopover();
if (hits.length !== 0) {
coolingDown = false;
}
}
};
xhttp.open("GET", "/scroll?scroll_id=" + scroll_id, true);
xhttp.send();
coolingDown = true;
}
}
});
function getSelectedMimeTypes() {
let mimeTypes = [];
let selected = tree.selected();
for (let i = 0; i < selected.length; i++) {
if(selected[i].id === "any") {
return "any"
}
//Only get children
if (selected[i].text.indexOf("(") !== -1) {
mimeTypes.push(selected[i].id);
}
}
return mimeTypes
}
function search() {
if (searchQueued === true) {
searchQueued = false;
//Clear old search results
let searchResults = document.getElementById("searchResults");
while (searchResults.firstChild) {
searchResults.removeChild(searchResults.firstChild);
}
let query = searchBar.value;
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
let searchResult = JSON.parse(this.responseText);
scroll_id = searchResult["_scroll_id"];
//Search stats
searchResults.appendChild(makeStatsCard(searchResult));
//Autocomplete
if (searchResult.hasOwnProperty("suggest") && searchResult["suggest"].hasOwnProperty("path")) {
pathAutoComplete = [];
for (let i = 0; i < searchResult["suggest"]["path"][0]["options"].length; i++) {
pathAutoComplete.push(searchResult["suggest"]["path"][0]["options"][i].text)
}
}
//Setup page
let resultContainer = makeResultContainer();
searchResults.appendChild(resultContainer);
//Insert search results (hits)
docCount = 0;
insertHits(resultContainer, searchResult["hits"]["hits"]);
//Initialise download/view button popover
initPopover();
}
};
xhttp.open("POST", "/search", true);
let postBody = {};
postBody.q = query;
postBody.size_min = size_min;
postBody.size_max = size_max;
postBody.mime_types = getSelectedMimeTypes();
postBody.must_match = must_match;
postBody.directories = selectedDirs;
postBody.path = pathBar.value.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
xhttp.setRequestHeader('content-type', 'application/json');
xhttp.send(JSON.stringify(postBody));
}
}
let pathAutoComplete = [];
let size_min = 0;
let size_max = 10000000000000;
searchBar.addEventListener("keyup", function () {
searchQueued = true;
});
//Size slider
let sizeSlider = $("#sizeSlider").ionRangeSlider({
type: "double",
grid: false,
force_edges: true,
min: 0,
max: 3684.03149864,
from: 0,
to: 3684.03149864,
min_interval: 5,
drag_interval: true,
prettify: function (num) {
if(num === 0) {
return "0 B"
} else if (num >= 3684) {
return humanFileSize(num * num * num) + "+";
}
return humanFileSize(num * num * num)
},
onChange: function(e) {
size_min = (e.from * e.from * e.from);
size_max = (e.to * e.to * e.to);
if (e.to >= 3684) {
size_max = 10000000000000;
}
searchQueued = true;
}
})[0];
//Directories select
function updateDirectories() {
let selected = $('#directories').find('option:selected');
selectedDirs = [];
$(selected).each(function(){
selectedDirs.push(parseInt($(this).val()));
});
searchQueued = true;
}
document.getElementById("directories").addEventListener("change", updateDirectories);
updateDirectories();
searchQueued = false;
//Suggest
function getPathChoices() {
return new Promise(getPaths => {
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
getPaths(JSON.parse(xhttp.responseText))
}
};
xhttp.open("GET", "/suggest?prefix=" + pathBar.value, true);
xhttp.send();
});
}
window.setInterval(search, 75)
var mimeMap = {{ mime_map | tojson }};
</script>
<script src="/static/js/search.js"></script>
</div>
{% endblock body %}

View File

@ -74,8 +74,8 @@
return;
}
var currentTask = JSON.parse(this.responseText);
var percent = currentTask.parsed / currentTask.total * 100;
let currentTask = JSON.parse(this.responseText);
let percent = currentTask.parsed / currentTask.total * 100;
try {
@ -84,7 +84,7 @@
document.getElementById("task-label-" + currentTask.id).innerHTML = "Calculating file count...";
} else {
var bar = document.getElementById("task-bar-" + currentTask.id);
let bar = document.getElementById("task-bar-" + currentTask.id);
bar.setAttribute("style", "width: " + percent + "%;");
document.getElementById("task-label-" + currentTask.id).innerHTML = currentTask.parsed + " / " + currentTask.total + " (" + percent.toFixed(2) + "%)";
@ -93,8 +93,6 @@
}
}
} catch (e) {
window.reload();
}

View File

@ -37,12 +37,13 @@ class ThumbnailGenerator:
try:
(ffmpeg.
input(path)
.overwrite_output("tmp", vframes=1, f="image2", loglevel="error")
.output("tmp", vframes=1, f="image2", loglevel="error")
.run()
)
self.generate_image("tmp", dest_path)
os.remove("tmp")
except Exception as e:
print(e)
print("Couldn't make thumbnail for " + path)
def generate_all(self, docs, dest_path, counter: Value=None):

View File

@ -1,28 +0,0 @@
Ajouter un utilisateur
mettre admin
Enlever admin
ne marche pas si t'est le seul admin
y'existe conn.executescript
Utiliser des functions queries pour afficher genre le total size of query, etc
Utiliser opendirectories-bot pour afficher des info
Plugins
MP3 tags
todo: other music
Font files
images
video tags
use es filter to filter out folders that the user has no permission to search
option to toggle auto complete
option to set password loop count
option to chose checksum thingy
option to chose mime guesser
option to toggle search history/stats
thumbnails are stored in a folder for each folder: easy to delete