Rewrite using fastapi
This commit is contained in:
commit
4291c845d8
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM recoll
|
||||||
|
|
||||||
|
RUN apt-get install breeze-icon-theme
|
||||||
|
|
||||||
|
COPY . /opt/squirrel
|
||||||
|
WORKDIR /opt/squirrel
|
||||||
|
|
||||||
|
RUN pip install --break-system-packages -e .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/bash"]
|
||||||
14
app/static/css/search-source.css
Normal file
14
app/static/css/search-source.css
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
SPAN.rclmatch {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--color-red-900);
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: beyondTheMountains; /* set name */
|
||||||
|
src: url(/static/fonts/beyond_the_mountains.ttf); /* url of the font */
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-beyond-the-mountains {
|
||||||
|
font-family: beyondTheMountains;
|
||||||
|
}
|
||||||
368
app/static/css/search.css
Normal file
368
app/static/css/search.css
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
@layer theme, base, components, utilities;
|
||||||
|
@layer theme {
|
||||||
|
:root, :host {
|
||||||
|
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji';
|
||||||
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
|
monospace;
|
||||||
|
--color-red-900: oklch(0.396 0.141 25.723);
|
||||||
|
--color-slate-600: oklch(0.446 0.043 257.281);
|
||||||
|
--color-gray-700: oklch(0.373 0.034 259.733);
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
--text-sm: 0.875rem;
|
||||||
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
|
--text-xl: 1.25rem;
|
||||||
|
--text-xl--line-height: calc(1.75 / 1.25);
|
||||||
|
--text-2xl: 1.5rem;
|
||||||
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
|
--text-8xl: 6rem;
|
||||||
|
--text-8xl--line-height: 1;
|
||||||
|
--leading-tight: 1.25;
|
||||||
|
--default-font-family: var(--font-sans);
|
||||||
|
--default-font-feature-settings: var(--font-sans--font-feature-settings);
|
||||||
|
--default-font-variation-settings: var(--font-sans--font-variation-settings);
|
||||||
|
--default-mono-font-family: var(--font-mono);
|
||||||
|
--default-mono-font-feature-settings: var(--font-mono--font-feature-settings);
|
||||||
|
--default-mono-font-variation-settings: var(--font-mono--font-variation-settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer base {
|
||||||
|
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0 solid;
|
||||||
|
}
|
||||||
|
html, :host {
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
tab-size: 4;
|
||||||
|
font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' );
|
||||||
|
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||||
|
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
color: inherit;
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
abbr:where([title]) {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
-webkit-text-decoration: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
b, strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
code, kbd, samp, pre {
|
||||||
|
font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace );
|
||||||
|
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||||
|
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
sub, sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
border-color: inherit;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
ol, ul, menu {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
img, svg, video, canvas, audio, iframe, embed, object {
|
||||||
|
display: block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
img, video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
button, input, select, optgroup, textarea, ::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
font-feature-settings: inherit;
|
||||||
|
font-variation-settings: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
:where(select:is([multiple], [size])) optgroup {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
:where(select:is([multiple], [size])) optgroup option {
|
||||||
|
padding-inline-start: 20px;
|
||||||
|
}
|
||||||
|
::file-selector-button {
|
||||||
|
margin-inline-end: 4px;
|
||||||
|
}
|
||||||
|
::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
color: color-mix(in oklab, currentColor 50%, transparent);
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
::-webkit-date-and-time-value {
|
||||||
|
min-height: 1lh;
|
||||||
|
text-align: inherit;
|
||||||
|
}
|
||||||
|
::-webkit-datetime-edit {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
::-webkit-datetime-edit-fields-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||||
|
padding-block: 0;
|
||||||
|
}
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
|
||||||
|
appearance: button;
|
||||||
|
}
|
||||||
|
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
[hidden]:where(:not([hidden='until-found'])) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer utilities {
|
||||||
|
.m-2 {
|
||||||
|
margin: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.m-4 {
|
||||||
|
margin: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.mx-10 {
|
||||||
|
margin-inline: calc(var(--spacing) * 10);
|
||||||
|
}
|
||||||
|
.mx-auto {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
.my-10 {
|
||||||
|
margin-block: calc(var(--spacing) * 10);
|
||||||
|
}
|
||||||
|
.mt-50 {
|
||||||
|
margin-top: calc(var(--spacing) * 50);
|
||||||
|
}
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.h-16 {
|
||||||
|
height: calc(var(--spacing) * 16);
|
||||||
|
}
|
||||||
|
.w-1\/2 {
|
||||||
|
width: calc(1/2 * 100%);
|
||||||
|
}
|
||||||
|
.w-2\/3 {
|
||||||
|
width: calc(2/3 * 100%);
|
||||||
|
}
|
||||||
|
.w-16 {
|
||||||
|
width: calc(var(--spacing) * 16);
|
||||||
|
}
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.appearance-none {
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: calc(infinity * 1px);
|
||||||
|
}
|
||||||
|
.border-b {
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
.border-red-900 {
|
||||||
|
border-color: var(--color-red-900);
|
||||||
|
}
|
||||||
|
.border-slate-600 {
|
||||||
|
border-color: var(--color-slate-600);
|
||||||
|
}
|
||||||
|
.p-4 {
|
||||||
|
padding: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.px-3 {
|
||||||
|
padding-inline: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.px-5 {
|
||||||
|
padding-inline: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
|
.py-2 {
|
||||||
|
padding-block: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.py-3 {
|
||||||
|
padding-block: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.pb-2 {
|
||||||
|
padding-bottom: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.text-2xl {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||||
|
}
|
||||||
|
.text-8xl {
|
||||||
|
font-size: var(--text-8xl);
|
||||||
|
line-height: var(--tw-leading, var(--text-8xl--line-height));
|
||||||
|
}
|
||||||
|
.text-sm {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||||
|
}
|
||||||
|
.text-xl {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||||
|
}
|
||||||
|
.leading-tight {
|
||||||
|
--tw-leading: var(--leading-tight);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
}
|
||||||
|
.text-gray-700 {
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
.text-red-900 {
|
||||||
|
color: var(--color-red-900);
|
||||||
|
}
|
||||||
|
.shadow {
|
||||||
|
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
.focus\:outline-none {
|
||||||
|
&:focus {
|
||||||
|
--tw-outline-style: none;
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SPAN.rclmatch {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--color-red-900);
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: beyondTheMountains;
|
||||||
|
src: url(/static/fonts/beyond_the_mountains.ttf);
|
||||||
|
}
|
||||||
|
.font-beyond-the-mountains {
|
||||||
|
font-family: beyondTheMountains;
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-leading {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-ring-inset {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-width {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0px;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #fff;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
BIN
app/static/fonts/beyond_the_mountains.ttf
Normal file
BIN
app/static/fonts/beyond_the_mountains.ttf
Normal file
Binary file not shown.
BIN
app/static/images/squirrel_32.png
Normal file
BIN
app/static/images/squirrel_32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
13
app/templates/base.html
Normal file
13
app/templates/base.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<html>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Squirrel</title>
|
||||||
|
<link rel="icon" type="image/png" href="/static/images/squirrel_32.png">
|
||||||
|
<link rel="stylesheet" href="/static/css/search.css"></link>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}CONTENT{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
app/templates/home.html
Normal file
17
app/templates/home.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="home-header"></div>
|
||||||
|
<div id="home-search" class="mx-auto mt-50">
|
||||||
|
<div class="font-beyond-the-mountains text-8xl text-red-900 text-center">Squirrel</div>
|
||||||
|
<div class="mx-auto w-1/2 my-10">
|
||||||
|
<form action="/search" method="get">
|
||||||
|
<input type="text" name="q" placeholder="Find your nuts..."
|
||||||
|
class="shadow appearance-none border-slate-600 text-2xl rounded-full w-full py-3 px-5 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||||
|
></input>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
54
app/templates/search_results.html
Normal file
54
app/templates/search_results.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div x-data="query">
|
||||||
|
<div id="search-header" class="flex m-2 items-center border-b pb-2 border-red-900 mx-10">
|
||||||
|
<a href="/" class="mx-10 text-2xl text-red-900 font-beyond-the-mountains">Squirrel</a>
|
||||||
|
<form action="/search" method="get">
|
||||||
|
<input
|
||||||
|
class="shadow appearance-none border-slate-600 rounded-full w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||||
|
type="text" size="80" name="q"
|
||||||
|
onfocus="this.select()"
|
||||||
|
x-bind:value="query"></input>
|
||||||
|
</form>
|
||||||
|
<div class="grow text-right">
|
||||||
|
Foo
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-main" class="mx-auto w-2/3">
|
||||||
|
<div id="search-info" class="flex">
|
||||||
|
<p class="grow"></p>
|
||||||
|
<p>Page <span x-text="page"></span> of <span x-text="pages"></span>,
|
||||||
|
<span x-text="total"></span> results.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-results">
|
||||||
|
<template x-for="result in results">
|
||||||
|
<div class="p-4 m-4">
|
||||||
|
<div class="m-2 flex">
|
||||||
|
<img class="w-16 h-16 m-2" x-bind:src="'/icons/' + result.mtype.replace('/', '-') + '.svg'"></img>
|
||||||
|
<div>
|
||||||
|
<a class="text-xl text-red-900" x-text="result.title" x-bind:href="'/docs/' + result.fpath" target="_blank"></a><br/>
|
||||||
|
<span class="text-sm" x-text="result.relevancyrating"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<template x-for="snippet in result.snippets">
|
||||||
|
<span class="text-sm"><span x-html="snippet[2]"></span><br/></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-footer" class="text-sm">
|
||||||
|
XQuery: <span x-text="xquery"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<script>
|
||||||
|
var query = {{ search|tojson }};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[project]
|
||||||
|
name = "squirrel"
|
||||||
|
version = "0.3.0"
|
||||||
|
description = "Find your nuts"
|
||||||
|
authors = [
|
||||||
|
{name = "Your Name",email = "you@example.com"}
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi[standard] (>=0.118.0,<0.119.0)"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
"squirrel" = "squirrel.server:daemon"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
0
squirrel/__init__.py
Normal file
0
squirrel/__init__.py
Normal file
101
squirrel/api.py
Normal file
101
squirrel/api.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
from math import ceil
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from typing import Any
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from recoll import recoll
|
||||||
|
|
||||||
|
from fastapi import BackgroundTasks
|
||||||
|
|
||||||
|
# from .config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
DOC_FOLDER = "/mnt/docs"
|
||||||
|
|
||||||
|
STRIP_CHARS = len(DOC_FOLDER) + len("file:///")
|
||||||
|
|
||||||
|
|
||||||
|
class Recoll(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._indexing = False
|
||||||
|
|
||||||
|
def reindex(self):
|
||||||
|
if self._indexing:
|
||||||
|
raise RuntimeError("Reindex already underway")
|
||||||
|
self._indexing = True
|
||||||
|
subprocess.call(["recollindex"])
|
||||||
|
self._indexing = False
|
||||||
|
|
||||||
|
|
||||||
|
repo = Recoll()
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.mount("/static", StaticFiles(directory="app/static"))
|
||||||
|
app.mount("/docs", StaticFiles(directory=DOC_FOLDER))
|
||||||
|
app.mount(
|
||||||
|
"/icons",
|
||||||
|
StaticFiles(directory="/usr/share/icons/breeze/mimetypes/32", follow_symlink=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def home(request: Request):
|
||||||
|
ctx = {"request": request}
|
||||||
|
return templates.TemplateResponse("home.html", context=ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/search")
|
||||||
|
def search_results(request: Request, q: str, count: int = 20, page: int = 1):
|
||||||
|
ctx: dict[str, Any] = {"request": request}
|
||||||
|
ctx["search"] = search(q, count, page)
|
||||||
|
return templates.TemplateResponse("search_results.html", context=ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/reindex")
|
||||||
|
async def get_reindex(tasks: BackgroundTasks) -> dict[str, str]:
|
||||||
|
tasks.add_task(repo.reindex)
|
||||||
|
return {"message": "Recoll index started"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/search")
|
||||||
|
def get_search(q: str, count: int = 20, page: int = 1) -> dict[str, str]:
|
||||||
|
return search(q, count, page)
|
||||||
|
|
||||||
|
|
||||||
|
def search(q: str, count: int, page: int):
|
||||||
|
db = recoll.connect()
|
||||||
|
query = db.query()
|
||||||
|
nres = query.execute(q)
|
||||||
|
result = {
|
||||||
|
"query": q,
|
||||||
|
"count": count,
|
||||||
|
"page": page,
|
||||||
|
"pages": ceil(nres / count),
|
||||||
|
"total": nres,
|
||||||
|
"xquery": query.getxquery(),
|
||||||
|
"results": [],
|
||||||
|
}
|
||||||
|
if page > result["pages"] or page < 1:
|
||||||
|
return result
|
||||||
|
|
||||||
|
query.scroll((page - 1) * count)
|
||||||
|
docs = query.fetchmany(count)
|
||||||
|
for doc in docs:
|
||||||
|
d = dict(doc)
|
||||||
|
d["snippets"] = query.getsnippets(doc, ctxwords=20)
|
||||||
|
d["fpath"] = d["url"][STRIP_CHARS:]
|
||||||
|
result["results"].append(d)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/info")
|
||||||
|
def get_info():
|
||||||
|
db = recoll.connect()
|
||||||
|
return dir(db)
|
||||||
12
squirrel/config.py
Normal file
12
squirrel/config.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from os.path import abspath
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
app_name: str = "Squirrel"
|
||||||
|
items_per_user: int = 50
|
||||||
|
archive: str = "./archive"
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
settings.archive = abspath(settings.archive)
|
||||||
9
squirrel/server.py
Normal file
9
squirrel/server.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import uvicorn
|
||||||
|
|
||||||
|
|
||||||
|
def daemon(reload=False):
|
||||||
|
uvicorn.run("squirrel.api:app", host="0.0.0.0", port=8000, reload=reload)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
daemon(True)
|
||||||
Loading…
x
Reference in New Issue
Block a user