diff --git a/app/src/api.js b/app/src/api.js
index 7f32a504..ed1d8db6 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -661,3 +661,18 @@ export async function getAPIStatus() {
let response = await apiGet(`${API_URI}/echo`);
return response.status === 200;
}
+
+export async function getFileStatistics() {
+ let response = await apiGet(`${API_URI}/files/statistics`);
+ if (response.status === 200) {
+ const contents = await response.json();
+ return contents['statistics'];
+ } else {
+ toast({
+ type: 'error',
+ title: 'Error!',
+ description: 'Unable to get file statistics',
+ time: 5000,
+ })
+ }
+}
diff --git a/app/src/components/Apps.js b/app/src/components/Apps.js
index 04ce221f..efc4a334 100644
--- a/app/src/components/Apps.js
+++ b/app/src/components/Apps.js
@@ -1,11 +1,12 @@
import React, {useContext} from 'react';
-import {Divider, SegmentGroup} from "semantic-ui-react";
+import {Divider, SegmentGroup, StatisticLabel, StatisticValue} from "semantic-ui-react";
import {Route, Routes} from "react-router-dom";
import "../static/wrolpi.css";
import {decryptOTP, encryptOTP} from "../api";
import {PageContainer, useTitle} from "./Common";
import {ThemeContext} from "../contexts/contexts";
-import {Button, Header, Segment, TextArea} from "./Theme";
+import {Button, Header, Loader, Segment, Statistic, StatisticGroup, TextArea} from "./Theme";
+import {useFileStatistics} from "../hooks/customHooks";
class Encrypt extends React.Component {
constructor(props) {
@@ -145,10 +146,72 @@ function OTP() {
>
}
+function FileStatistics() {
+ useTitle('File Statistics');
+
+ const {s} = useContext(ThemeContext);
+
+ const {statistics} = useFileStatistics();
+
+ let body = ;
+
+ if (statistics === undefined) {
+ body =
Failed to fetch statistics
;
+ }
+
+ if (statistics !== undefined && statistics !== null) {
+ const {total_count, video_count, pdf_count, ebook_count, archive_count, image_count, zip_count} = statistics;
+ body = <>
+
+
+ {total_count}
+ All Files
+
+
+
+
+ {video_count}
+ Videos
+
+
+ {pdf_count}
+ PDFs
+
+
+ {ebook_count}
+ eBooks
+
+
+ {archive_count}
+ Archives
+
+
+ {image_count}
+ Images
+
+
+
+
+ {zip_count}
+ ZIP
+
+
+ >;
+ }
+
+ return <>
+
+
+ {body}
+
+ >
+}
+
export function AppsRoute(props) {
return (
}/>
+ }/>
)
}
diff --git a/app/src/components/Nav.js b/app/src/components/Nav.js
index 15a0ee5c..1a4dccef 100644
--- a/app/src/components/Nav.js
+++ b/app/src/components/Nav.js
@@ -16,6 +16,7 @@ const links = [
{
key: 'apps', text: 'Apps', links: [
{to: '/apps/otp', text: 'One Time Pad', end: true},
+ {to: '/apps/file_statistics', text: 'File Statistics', end: true},
]
},
];
diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js
index 86f2b28c..dbcbc961 100644
--- a/app/src/hooks/customHooks.js
+++ b/app/src/hooks/customHooks.js
@@ -10,6 +10,7 @@ import {
getDownloaders,
getDownloads,
getFiles,
+ getFileStatistics,
getInventory,
getSettings,
getStatistics,
@@ -718,3 +719,22 @@ export const useInventory = (inventoryId) => {
return {byCategory, bySubcategory, byName, fetchInventory}
}
+
+export const useFileStatistics = () => {
+ const [statistics, setStatistics] = useState();
+
+ const fetchFileStatistics = async () => {
+ try {
+ const statistics = await getFileStatistics();
+ setStatistics(statistics || undefined);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ useEffect(() => {
+ fetchFileStatistics();
+ }, []);
+
+ return {statistics};
+}
diff --git a/wrolpi/files/api.py b/wrolpi/files/api.py
index 0398856d..5cbe33e3 100644
--- a/wrolpi/files/api.py
+++ b/wrolpi/files/api.py
@@ -135,3 +135,12 @@ def directories(_, body: schema.DirectoriesRequest):
body = {'directories': dirs, 'exists': path.exists(), 'is_dir': path.is_dir(), 'is_file': path.is_file()}
return response.json(body)
+
+
+@bp.get('/statistics')
+@openapi.definition(
+ summary='Get summary statistics of all files',
+)
+async def get_statistics(_):
+ statistics = await lib.get_file_statistics()
+ return json_response({'statistics': statistics})
diff --git a/wrolpi/files/lib.py b/wrolpi/files/lib.py
index fb03901b..5b0a1c9c 100644
--- a/wrolpi/files/lib.py
+++ b/wrolpi/files/lib.py
@@ -621,3 +621,26 @@ def split_file_name_words(name: str) -> List[str]:
except Exception as e:
logger.error(f'Failed to split filename into words: {name}', exc_info=e)
return [name]
+
+
+async def get_file_statistics():
+ with get_db_curs() as curs:
+ curs.execute('''
+ SELECT
+ COUNT(path) AS "total_count",
+ COUNT(path) FILTER (WHERE file.mimetype LIKE 'video/%') AS "video_count",
+ COUNT(path) FILTER (WHERE file.mimetype = 'application/pdf') AS "pdf_count",
+ COUNT(path) FILTER (WHERE file.mimetype LIKE 'image/%' AND file.associated = FALSE) AS "image_count",
+ COUNT(path) FILTER (WHERE file.mimetype = 'application/zip') AS "zip_count"
+ FROM
+ file
+ ''')
+ statistics = dict(curs.fetchall()[0])
+
+ curs.execute('SELECT COUNT(*) FROM archive')
+ statistics['archive_count'] = curs.fetchall()[0][0]
+
+ curs.execute('SELECT COUNT(*) FROM ebook')
+ statistics['ebook_count'] = curs.fetchall()[0][0]
+
+ return statistics
diff --git a/wrolpi/files/test/test_api.py b/wrolpi/files/test/test_api.py
index b9158e5c..811d2743 100644
--- a/wrolpi/files/test/test_api.py
+++ b/wrolpi/files/test/test_api.py
@@ -242,3 +242,35 @@ def test_refresh_files_list(test_session, test_client, make_files_structure):
request, response = test_client.post('/api/files/refresh/list', content=content)
assert response.status_code == HTTPStatus.NO_CONTENT
assert test_session.query(File).count() == 1
+
+
+def test_file_statistics(test_session, test_client, example_pdf, example_mobi, example_epub, video_file):
+ """A summary of File statistics can be fetched."""
+ # Statistics can be fetched while empty.
+ request, response = test_client.get('/api/files/statistics')
+ assert response.status_code == HTTPStatus.OK
+ assert response.json == {
+ 'statistics': {
+ 'archive_count': 0,
+ 'ebook_count': 0,
+ 'image_count': 0,
+ 'pdf_count': 0,
+ 'total_count': 0,
+ 'video_count': 0,
+ 'zip_count': 0},
+ }
+
+ test_client.post('/api/files/refresh')
+
+ request, response = test_client.get('/api/files/statistics')
+ assert response.status_code == HTTPStatus.OK
+ assert response.json == {
+ 'statistics': {
+ 'archive_count': 0,
+ 'ebook_count': 2,
+ 'image_count': 0,
+ 'pdf_count': 1,
+ 'total_count': 5,
+ 'video_count': 1,
+ 'zip_count': 0},
+ }