mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
141a863079 | ||
|
|
6fa8d4be34 | ||
|
|
984e8a44d5 | ||
|
|
bdb296bce2 | ||
|
|
955a9084c3 | ||
|
|
70f486b0eb | ||
|
|
0bc383c6a8 | ||
|
|
0bb2b36585 | ||
|
|
9e3a24753a | ||
|
|
f2c400799e | ||
|
|
25a8c8a7e3 | ||
|
|
8f95d89fc6 | ||
|
|
39b0cdb9d5 | ||
|
|
42e06b848e | ||
|
|
7c3a13b1c4 | ||
|
|
151248fce2 | ||
|
|
6e8d6fe063 | ||
|
|
ca2da5e631 | ||
|
|
6cd3b70201 | ||
|
|
2dcfba75b5 | ||
|
|
44a51b03d3 | ||
|
|
6ee9e9831e | ||
|
|
68c717cee8 | ||
|
|
b15fc02c34 | ||
|
|
033ebfb497 | ||
|
|
4cceaa7650 | ||
|
|
5df47f1396 | ||
|
|
903f35c01b | ||
|
|
6a34f94277 | ||
|
|
dcc143eb7d | ||
|
|
fb47bf27e8 | ||
|
|
dcf969ff2e | ||
|
|
32c1318355 | ||
|
|
8ca6b89da4 | ||
|
|
b46c3a15f3 | ||
|
|
cbc5e014f7 | ||
|
|
8925b248e4 | ||
|
|
cc6aa2bbc5 |
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -48,19 +48,19 @@ jobs:
|
||||
run: mkdir -p target/pages/documentation/custom-css
|
||||
|
||||
- name: Convert readme file to html
|
||||
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
|
||||
uses: jaywcjlove/markdown-to-html-cli@cff9330af4ca8048b197a76d9eb1db189c2a7cee # v5.0.4
|
||||
with:
|
||||
source: README.md
|
||||
output: target/pages/index.html
|
||||
|
||||
- name: Convert config documentation to html
|
||||
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
|
||||
uses: jaywcjlove/markdown-to-html-cli@cff9330af4ca8048b197a76d9eb1db189c2a7cee # v5.0.4
|
||||
with:
|
||||
source: commafeed-server/target/quarkus-generated-doc/config/commafeed-server.md
|
||||
output: target/pages/documentation/index.html
|
||||
|
||||
- name: Convert custom css documentation to html
|
||||
uses: jaywcjlove/markdown-to-html-cli@d2c8ffd676de1801e2586904bc540a938e4bc480 # v5.0.3
|
||||
uses: jaywcjlove/markdown-to-html-cli@cff9330af4ca8048b197a76d9eb1db189c2a7cee # v5.0.4
|
||||
with:
|
||||
source: documentation/CUSTOMCSS.md
|
||||
output: target/pages/documentation/custom-css/index.html
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
steps:
|
||||
# Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -249,7 +249,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [6.2.0]
|
||||
|
||||
- Starred entries are no longer deleted after a certain amount of time, they are now kept indefinitely. The new `commafeed.database.cleanup.keep-starred-entries` setting can be disabled to restore the previous behavior if you want to keep deleting starred entries during normal entries cleanup (#1581)
|
||||
|
||||
## [6.1.1]
|
||||
|
||||
- Fix old starred entries not loading if they were marked as read (#2031)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 4,
|
||||
|
||||
1206
commafeed-client/package-lock.json
generated
1206
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,31 +17,31 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@fontsource/open-sans": "^5.2.7",
|
||||
"@lingui/core": "^5.8.0",
|
||||
"@lingui/react": "^5.8.0",
|
||||
"@mantine/core": "^8.3.13",
|
||||
"@mantine/form": "^8.3.13",
|
||||
"@mantine/hooks": "^8.3.13",
|
||||
"@mantine/modals": "^8.3.13",
|
||||
"@mantine/notifications": "^8.3.13",
|
||||
"@mantine/spotlight": "^8.3.13",
|
||||
"@lingui/core": "^5.9.0",
|
||||
"@lingui/react": "^5.9.0",
|
||||
"@mantine/core": "^8.3.14",
|
||||
"@mantine/form": "^8.3.14",
|
||||
"@mantine/hooks": "^8.3.14",
|
||||
"@mantine/modals": "^8.3.14",
|
||||
"@mantine/notifications": "^8.3.14",
|
||||
"@mantine/spotlight": "^8.3.14",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"axios": "^1.13.2",
|
||||
"axios": "^1.13.4",
|
||||
"dayjs": "^1.11.19",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"interweave": "^13.1.1",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^19.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-draggable": "^4.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.12.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"style-to-object": "^1.0.14",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
@@ -50,32 +50,32 @@
|
||||
"websocket-heartbeat-js": "^1.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.8.0",
|
||||
"@lingui/cli": "^5.8.0",
|
||||
"@lingui/vite-plugin": "^5.8.0",
|
||||
"@biomejs/biome": "^2.3.14",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.9.0",
|
||||
"@lingui/cli": "^5.9.0",
|
||||
"@lingui/vite-plugin": "^5.9.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react": "^19.2.13",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-infinite-scroller": "^1.2.5",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@types/tinycon": "^0.6.7",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitejs/plugin-react": "^5.1.3",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-checker": "^0.12.0",
|
||||
"vite-tsconfig-paths": "^6.0.4",
|
||||
"vite-tsconfig-paths": "^6.0.5",
|
||||
"vitest": "^4.0.18",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"overrides": {
|
||||
"react-infinite-scroller": {
|
||||
"react": "^19.2.3"
|
||||
"react": "^19.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>6.1.1</version>
|
||||
<version>6.2.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
@@ -15,7 +15,7 @@
|
||||
<!-- renovate: datasource=node-version depName=node -->
|
||||
<node.version>v24.13.0</node.version>
|
||||
<!-- renovate: datasource=npm depName=npm -->
|
||||
<npm.version>11.8.0</npm.version>
|
||||
<npm.version>11.9.0</npm.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -9,7 +9,7 @@ msgstr ""
|
||||
"Language: gl\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-12-27 15:30+0200\n"
|
||||
"PO-Revision-Date: 2026-02-04 05:30+0200\n"
|
||||
"Last-Translator: José M. <correoxm@disroot.org>\n"
|
||||
"Language-Team: gl\n"
|
||||
"Plural-Forms: \n"
|
||||
@@ -64,7 +64,7 @@ msgstr "Administración"
|
||||
#: src/pages/auth/InitialSetupPage.tsx
|
||||
#: src/pages/auth/InitialSetupPage.tsx
|
||||
msgid "Admin user name"
|
||||
msgstr ""
|
||||
msgstr "Identificador de admin"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
@@ -231,7 +231,7 @@ msgstr "Confirmar contrasinal"
|
||||
#: src/pages/auth/PasswordResetPage.tsx
|
||||
#: src/pages/auth/PasswordResetPage.tsx
|
||||
msgid "Confirm Password"
|
||||
msgstr ""
|
||||
msgstr "Confirmar contrasinal"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
@@ -239,7 +239,7 @@ msgstr "Acolledor"
|
||||
|
||||
#: src/pages/auth/InitialSetupPage.tsx
|
||||
msgid "Create Admin Account"
|
||||
msgstr ""
|
||||
msgstr "Crear conta Admin"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
@@ -496,11 +496,11 @@ msgstr "Índigo"
|
||||
|
||||
#: src/pages/auth/InitialSetupPage.tsx
|
||||
msgid "Initial Setup"
|
||||
msgstr ""
|
||||
msgstr "Configuración inicial"
|
||||
|
||||
#: src/pages/auth/PasswordResetPage.tsx
|
||||
msgid "Invalid password reset link. Please request a new one."
|
||||
msgstr ""
|
||||
msgstr "A ligazón de restablecemento non é válida. Solicita unha nova."
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
@@ -648,7 +648,7 @@ msgstr "Novo contrasinal"
|
||||
#: src/pages/auth/PasswordResetPage.tsx
|
||||
#: src/pages/auth/PasswordResetPage.tsx
|
||||
msgid "New Password"
|
||||
msgstr ""
|
||||
msgstr "Novo contrasinal"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
@@ -789,7 +789,7 @@ msgstr "Contrasinal"
|
||||
|
||||
#: src/hooks/useValidationRules.ts
|
||||
msgid "Password must be at least {minimumPasswordLength} characters"
|
||||
msgstr ""
|
||||
msgstr "O contrasinal ten que ter {minimumPasswordLength} caracteres polo menos"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
@@ -840,7 +840,7 @@ msgstr "Non se admiten novas contas nesta instancia de CommaFeed"
|
||||
#: src/pages/auth/PasswordResetPage.tsx
|
||||
#: src/pages/auth/PasswordResetPage.tsx
|
||||
msgid "Reset Password"
|
||||
msgstr ""
|
||||
msgstr "Restablecer contrasinal"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
@@ -1104,7 +1104,7 @@ msgstr "Páxina web"
|
||||
|
||||
#: src/pages/auth/InitialSetupPage.tsx
|
||||
msgid "Welcome! This appears to be the first time you're running CommaFeed. Please create an administrator account to get started."
|
||||
msgstr ""
|
||||
msgstr "Benvida! Semella que é a primeira vez que executas CommaFeed. Para comezar, crea unha conta de administración."
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Yellow"
|
||||
@@ -1120,4 +1120,4 @@ msgstr "As túas canles están na cola para ser actualizadas."
|
||||
|
||||
#: src/pages/auth/PasswordResetPage.tsx
|
||||
msgid "Your password has been changed. You can now log in with your new password."
|
||||
msgstr ""
|
||||
msgstr "O teu contrasinal cambiou. Xa podes acceder coas novas credenciais."
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>6.1.1</version>
|
||||
<version>6.2.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-server</artifactId>
|
||||
<name>CommaFeed Server</name>
|
||||
<packaging>quarkus</packaging>
|
||||
|
||||
<properties>
|
||||
<quarkus.version>3.30.8</quarkus.version>
|
||||
<quarkus.version>3.31.2</quarkus.version>
|
||||
<querydsl.version>7.1</querydsl.version>
|
||||
<rome.version>2.1.0</rome.version>
|
||||
|
||||
@@ -77,24 +78,14 @@
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
<goal>native-image-agent</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<properties>
|
||||
<quarkus.package.output-name>commafeed-${project.version}</quarkus.package.output-name>
|
||||
<quarkus.package.runner-suffix>
|
||||
-${build.database}-${os.detected.name}-${os.detected.arch}-runner
|
||||
</quarkus.package.runner-suffix>
|
||||
</properties>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<properties>
|
||||
<quarkus.package.output-name>commafeed-${project.version}</quarkus.package.output-name>
|
||||
<quarkus.package.runner-suffix>
|
||||
-${build.database}-${os.detected.name}-${os.detected.arch}-runner
|
||||
</quarkus.package.runner-suffix>
|
||||
</properties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>io.quarkus</groupId>
|
||||
@@ -167,6 +158,7 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.4</version>
|
||||
<configuration>
|
||||
<argLine>@{argLine}</argLine>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<quarkus.datasource.db-kind>${build.database}</quarkus.datasource.db-kind>
|
||||
@@ -186,14 +178,13 @@
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<argLine>@{argLine}</argLine>
|
||||
<systemPropertyVariables>
|
||||
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
|
||||
</native.image.path>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<quarkus.datasource.db-kind>${build.database}</quarkus.datasource.db-kind>
|
||||
</systemPropertyVariables>
|
||||
<!-- fix for java.lang.NoClassDefFoundError: Could not initialize class org.jboss.threads.JDKSpecific$ThreadAccess (#1938) -->
|
||||
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
|
||||
</configuration>
|
||||
<!-- failsafe plugin does not seem to be able to pick up dependencies declared in profiles -->
|
||||
<dependencies>
|
||||
@@ -246,7 +237,7 @@
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>13.0.0</version>
|
||||
<version>13.2.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
@@ -275,7 +266,7 @@
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<version>3.2.1</version>
|
||||
<?m2e ignore?>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -304,7 +295,7 @@
|
||||
<dependency>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<version>6.1.1</version>
|
||||
<version>6.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- compile-time processors -->
|
||||
@@ -475,7 +466,7 @@
|
||||
<!-- test dependencies -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||
<artifactId>quarkus-junit-mockito</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:13.3@sha256:5cf544fad978371b3df255b61e209b373583cb88b733475c86e49faa15ac2104
|
||||
FROM debian:13.3@sha256:2c91e484d93f0830a7e05a2b9d92a7b102be7cab562198b984a84fdbc7806d91
|
||||
ARG TARGETARCH
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
@@ -312,6 +312,12 @@ public interface CommaFeedConfiguration {
|
||||
@WithDefault("100")
|
||||
int batchSize();
|
||||
|
||||
/**
|
||||
* Whether to keep starred entries when cleaning up old entries.
|
||||
*/
|
||||
@WithDefault("true")
|
||||
boolean keepStarredEntries();
|
||||
|
||||
default Instant statusesInstantThreshold() {
|
||||
return statusesMaxAge().toMillis() > 0 ? Instant.now().minus(statusesMaxAge()) : null;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.commafeed.backend.model.FeedEntry;
|
||||
import com.commafeed.backend.model.QFeedEntry;
|
||||
import com.querydsl.core.Tuple;
|
||||
import com.querydsl.core.types.dsl.NumberExpression;
|
||||
import com.querydsl.jpa.impl.JPAQuery;
|
||||
|
||||
@Singleton
|
||||
public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
@@ -25,15 +26,21 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
return query().select(ENTRY).from(ENTRY).where(ENTRY.guidHash.eq(guidHash), ENTRY.feed.eq(feed)).limit(1).fetchOne();
|
||||
}
|
||||
|
||||
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max) {
|
||||
public List<FeedCapacity> findFeedsExceedingCapacity(long maxCapacity, long max, boolean keepStarredEntries) {
|
||||
NumberExpression<Long> count = ENTRY.id.count();
|
||||
List<Tuple> tuples = query().select(ENTRY.feed.id, count)
|
||||
.from(ENTRY)
|
||||
.groupBy(ENTRY.feed)
|
||||
JPAQuery<Tuple> query = query().select(ENTRY.feed.id, count).from(ENTRY);
|
||||
|
||||
if (keepStarredEntries) {
|
||||
query.where(Predicates.isNotStarred(ENTRY));
|
||||
}
|
||||
|
||||
return query.groupBy(ENTRY.feed)
|
||||
.having(count.gt(maxCapacity))
|
||||
.limit(max)
|
||||
.fetch();
|
||||
return tuples.stream().map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count))).toList();
|
||||
.fetch()
|
||||
.stream()
|
||||
.map(t -> new FeedCapacity(t.get(ENTRY.feed.id), t.get(count)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public int delete(Long feedId, long max) {
|
||||
@@ -44,21 +51,30 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
||||
/**
|
||||
* Delete entries older than a certain date
|
||||
*/
|
||||
public int deleteEntriesOlderThan(Instant olderThan, long max) {
|
||||
List<FeedEntry> list = query().selectFrom(ENTRY)
|
||||
public int deleteEntriesOlderThan(Instant olderThan, long max, boolean keepStarredEntries) {
|
||||
JPAQuery<FeedEntry> query = query().selectFrom(ENTRY)
|
||||
.where(ENTRY.published.lt(olderThan))
|
||||
.orderBy(ENTRY.published.asc())
|
||||
.limit(max)
|
||||
.fetch();
|
||||
return delete(list);
|
||||
.limit(max);
|
||||
|
||||
if (keepStarredEntries) {
|
||||
query.where(Predicates.isNotStarred(ENTRY));
|
||||
}
|
||||
|
||||
return delete(query.fetch());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the oldest entries of a feed
|
||||
*/
|
||||
public int deleteOldEntries(Long feedId, long max) {
|
||||
List<FeedEntry> list = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.published.asc()).limit(max).fetch();
|
||||
return delete(list);
|
||||
public int deleteOldEntries(Long feedId, long max, boolean keepStarredEntries) {
|
||||
JPAQuery<FeedEntry> query = query().selectFrom(ENTRY).where(ENTRY.feed.id.eq(feedId)).orderBy(ENTRY.published.asc()).limit(max);
|
||||
|
||||
if (keepStarredEntries) {
|
||||
query.where(Predicates.isNotStarred(ENTRY));
|
||||
}
|
||||
|
||||
return delete(query.fetch());
|
||||
}
|
||||
|
||||
public record FeedCapacity(Long id, Long capacity) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.commafeed.backend.dao;
|
||||
|
||||
import com.commafeed.backend.model.QFeedEntry;
|
||||
import com.commafeed.backend.model.QFeedEntryStatus;
|
||||
import com.querydsl.core.types.dsl.BooleanExpression;
|
||||
import com.querydsl.jpa.JPAExpressions;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class Predicates {
|
||||
|
||||
private static final QFeedEntryStatus STATUS = QFeedEntryStatus.feedEntryStatus;
|
||||
|
||||
public static BooleanExpression isNotStarred(QFeedEntry entry) {
|
||||
return JPAExpressions.selectOne().from(STATUS).where(STATUS.entry.eq(entry).and(STATUS.starred.isTrue())).notExists();
|
||||
}
|
||||
}
|
||||
@@ -27,13 +27,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Singleton
|
||||
public class DatabaseCleaningService {
|
||||
|
||||
private final int batchSize;
|
||||
|
||||
private final UnitOfWork unitOfWork;
|
||||
private final FeedDAO feedDAO;
|
||||
private final FeedEntryDAO feedEntryDAO;
|
||||
private final FeedEntryContentDAO feedEntryContentDAO;
|
||||
private final FeedEntryStatusDAO feedEntryStatusDAO;
|
||||
private final int batchSize;
|
||||
private final boolean keepStarredEntries;
|
||||
private final Meter entriesDeletedMeter;
|
||||
|
||||
public DatabaseCleaningService(CommaFeedConfiguration config, UnitOfWork unitOfWork, FeedDAO feedDAO, FeedEntryDAO feedEntryDAO,
|
||||
@@ -44,6 +44,7 @@ public class DatabaseCleaningService {
|
||||
this.feedEntryContentDAO = feedEntryContentDAO;
|
||||
this.feedEntryStatusDAO = feedEntryStatusDAO;
|
||||
this.batchSize = config.database().cleanup().batchSize();
|
||||
this.keepStarredEntries = config.database().cleanup().keepStarredEntries();
|
||||
this.entriesDeletedMeter = metrics.meter(MetricRegistry.name(getClass(), "entriesDeleted"));
|
||||
}
|
||||
|
||||
@@ -86,21 +87,23 @@ public class DatabaseCleaningService {
|
||||
log.info("cleaning entries exceeding feed capacity");
|
||||
long total = 0;
|
||||
while (true) {
|
||||
List<FeedCapacity> feeds = unitOfWork.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize));
|
||||
List<FeedCapacity> feeds = unitOfWork
|
||||
.call(() -> feedEntryDAO.findFeedsExceedingCapacity(maxFeedCapacity, batchSize, keepStarredEntries));
|
||||
if (feeds.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (final FeedCapacity feed : feeds) {
|
||||
long remaining = feed.capacity() - maxFeedCapacity;
|
||||
int deleted;
|
||||
do {
|
||||
final long rem = remaining;
|
||||
int deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem)));
|
||||
deleted = unitOfWork.call(() -> feedEntryDAO.deleteOldEntries(feed.id(), Math.min(batchSize, rem), keepStarredEntries));
|
||||
entriesDeletedMeter.mark(deleted);
|
||||
total += deleted;
|
||||
remaining -= deleted;
|
||||
log.debug("removed {} entries for feeds exceeding capacity", total);
|
||||
} while (remaining > 0);
|
||||
} while (deleted > 0 && remaining > 0);
|
||||
}
|
||||
}
|
||||
log.info("cleanup done: {} entries for feeds exceeding capacity deleted", total);
|
||||
@@ -111,7 +114,7 @@ public class DatabaseCleaningService {
|
||||
long total = 0;
|
||||
long deleted;
|
||||
do {
|
||||
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize));
|
||||
deleted = unitOfWork.call(() -> feedEntryDAO.deleteEntriesOlderThan(olderThan, batchSize, keepStarredEntries));
|
||||
entriesDeletedMeter.mark(deleted);
|
||||
total += deleted;
|
||||
log.debug("removed {} old entries", total);
|
||||
|
||||
@@ -115,13 +115,13 @@ class DatabaseCleaningServiceTest {
|
||||
Mockito.when(feed2.id()).thenReturn(2L);
|
||||
Mockito.when(feed2.capacity()).thenReturn(120L);
|
||||
|
||||
Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE))
|
||||
Mockito.when(feedEntryDAO.findFeedsExceedingCapacity(50, BATCH_SIZE, false))
|
||||
.thenReturn(Arrays.asList(feed1, feed2))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100)).thenReturn(80);
|
||||
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50)).thenReturn(50);
|
||||
Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70)).thenReturn(70);
|
||||
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 100, false)).thenReturn(80);
|
||||
Mockito.when(feedEntryDAO.deleteOldEntries(1L, 50, false)).thenReturn(50);
|
||||
Mockito.when(feedEntryDAO.deleteOldEntries(2L, 70, false)).thenReturn(70);
|
||||
|
||||
service.cleanEntriesForFeedsExceedingCapacity(50);
|
||||
|
||||
@@ -132,11 +132,11 @@ class DatabaseCleaningServiceTest {
|
||||
void cleanEntriesOlderThanDeletesOldEntries() {
|
||||
Instant cutoff = LocalDate.now().minusDays(30).atStartOfDay().toInstant(ZoneOffset.UTC);
|
||||
|
||||
Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE)).thenReturn(100, 50, 0);
|
||||
Mockito.when(feedEntryDAO.deleteEntriesOlderThan(cutoff, BATCH_SIZE, false)).thenReturn(100, 50, 0);
|
||||
|
||||
service.cleanEntriesOlderThan(cutoff);
|
||||
|
||||
Mockito.verify(feedEntryDAO, Mockito.times(3)).deleteEntriesOlderThan(cutoff, BATCH_SIZE);
|
||||
Mockito.verify(feedEntryDAO, Mockito.times(3)).deleteEntriesOlderThan(cutoff, BATCH_SIZE, false);
|
||||
Mockito.verify(entriesDeletedMeter, Mockito.times(3)).mark(Mockito.anyLong());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.commafeed.integration.cleanup;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.commafeed.TestConstants;
|
||||
import com.commafeed.backend.service.db.DatabaseCleaningService;
|
||||
import com.commafeed.frontend.model.Entries;
|
||||
import com.commafeed.frontend.model.Entry;
|
||||
import com.commafeed.frontend.model.request.StarRequest;
|
||||
import com.commafeed.frontend.resource.CategoryREST;
|
||||
import com.commafeed.integration.BaseIT;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
|
||||
@QuarkusTest
|
||||
class DatabaseCleaningIT extends BaseIT {
|
||||
|
||||
@Inject
|
||||
DatabaseCleaningService databaseCleaningService;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
initialSetup(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
|
||||
RestAssured.authentication = RestAssured.preemptive().basic(TestConstants.ADMIN_USERNAME, TestConstants.ADMIN_PASSWORD);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
RestAssured.reset();
|
||||
}
|
||||
|
||||
private void starEntry(String entryId, Long subscriptionId) {
|
||||
StarRequest starRequest = new StarRequest();
|
||||
starRequest.setId(entryId);
|
||||
starRequest.setFeedId(subscriptionId);
|
||||
starRequest.setStarred(true);
|
||||
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star").then().statusCode(200);
|
||||
}
|
||||
|
||||
private void unstarEntry(String entryId, Long subscriptionId) {
|
||||
StarRequest starRequest = new StarRequest();
|
||||
starRequest.setId(entryId);
|
||||
starRequest.setFeedId(subscriptionId);
|
||||
starRequest.setStarred(false);
|
||||
RestAssured.given().body(starRequest).contentType(ContentType.JSON).post("rest/entry/star").then().statusCode(200);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class KeepStarredEntries {
|
||||
|
||||
@Test
|
||||
void starredEntriesAreKeptWhenCleaningFeedsExceedingCapacity() {
|
||||
// Subscribe to feed and wait for entries
|
||||
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||
|
||||
// Verify we have 2 entries
|
||||
Entries entriesBefore = getFeedEntries(subscriptionId);
|
||||
Assertions.assertEquals(2, entriesBefore.getEntries().size());
|
||||
|
||||
// Star the first entry
|
||||
Entry entryToStar = entriesBefore.getEntries().getFirst();
|
||||
starEntry(entryToStar.getId(), subscriptionId);
|
||||
|
||||
// Verify the entry is starred
|
||||
Entries starredEntries = getCategoryEntries(CategoryREST.STARRED);
|
||||
Assertions.assertEquals(1, starredEntries.getEntries().size());
|
||||
Assertions.assertEquals(entryToStar.getId(), starredEntries.getEntries().getFirst().getId());
|
||||
|
||||
// Run cleanup with capacity of 0 (should delete all non-starred entries)
|
||||
// With keepStarredEntries=true (default), only non-starred entries are counted for capacity.
|
||||
// We have 2 entries, 1 starred and 1 non-starred. With capacity=0, the 1 non-starred entry exceeds capacity.
|
||||
databaseCleaningService.cleanEntriesForFeedsExceedingCapacity(0);
|
||||
|
||||
// Verify starred entry is still present
|
||||
Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED);
|
||||
Assertions.assertEquals(1, starredEntriesAfter.getEntries().size());
|
||||
Assertions.assertEquals(entryToStar.getId(), starredEntriesAfter.getEntries().getFirst().getId());
|
||||
|
||||
// Verify the non-starred entry was deleted (only starred entry should remain)
|
||||
Entries entriesAfter = getFeedEntries(subscriptionId);
|
||||
Assertions.assertEquals(1, entriesAfter.getEntries().size());
|
||||
Assertions.assertEquals(entryToStar.getId(), entriesAfter.getEntries().getFirst().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void starredEntriesAreKeptWhenCleaningOldEntries() {
|
||||
// Subscribe to feed and wait for entries
|
||||
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||
|
||||
// Verify we have 2 entries
|
||||
Entries entriesBefore = getFeedEntries(subscriptionId);
|
||||
Assertions.assertEquals(2, entriesBefore.getEntries().size());
|
||||
|
||||
// Star the first entry (oldest one based on published date in rss.xml)
|
||||
Entry entryToStar = entriesBefore.getEntries().getFirst();
|
||||
starEntry(entryToStar.getId(), subscriptionId);
|
||||
|
||||
// Verify the entry is starred
|
||||
Entries starredEntries = getCategoryEntries(CategoryREST.STARRED);
|
||||
Assertions.assertEquals(1, starredEntries.getEntries().size());
|
||||
|
||||
// Run cleanup for entries older than now (should try to delete all entries)
|
||||
// With keepStarredEntries=true (default), the starred entry should be preserved
|
||||
Instant olderThan = Instant.now().plus(Duration.ofDays(1));
|
||||
databaseCleaningService.cleanEntriesOlderThan(olderThan);
|
||||
|
||||
// Verify starred entry is still present
|
||||
Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED);
|
||||
Assertions.assertEquals(1, starredEntriesAfter.getEntries().size());
|
||||
Assertions.assertEquals(entryToStar.getId(), starredEntriesAfter.getEntries().getFirst().getId());
|
||||
|
||||
// Verify the non-starred entry was deleted
|
||||
Entries entriesAfter = getFeedEntries(subscriptionId);
|
||||
Assertions.assertEquals(1, entriesAfter.getEntries().size());
|
||||
Assertions.assertEquals(entryToStar.getId(), entriesAfter.getEntries().getFirst().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleStarredEntriesAreAllKept() {
|
||||
// Subscribe to feed and wait for entries
|
||||
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||
|
||||
// Verify we have 2 entries
|
||||
Entries entriesBefore = getFeedEntries(subscriptionId);
|
||||
Assertions.assertEquals(2, entriesBefore.getEntries().size());
|
||||
|
||||
// Star both entries
|
||||
entriesBefore.getEntries().forEach(entry -> starEntry(entry.getId(), subscriptionId));
|
||||
|
||||
// Verify both entries are starred
|
||||
Entries starredEntries = getCategoryEntries(CategoryREST.STARRED);
|
||||
Assertions.assertEquals(2, starredEntries.getEntries().size());
|
||||
|
||||
// Run cleanup with capacity of 0 (should delete all non-starred entries)
|
||||
databaseCleaningService.cleanEntriesForFeedsExceedingCapacity(0);
|
||||
|
||||
// Verify both starred entries are still present
|
||||
Entries starredEntriesAfter = getCategoryEntries(CategoryREST.STARRED);
|
||||
Assertions.assertEquals(2, starredEntriesAfter.getEntries().size());
|
||||
|
||||
// Verify all entries are preserved (since all are starred)
|
||||
Entries entriesAfter = getFeedEntries(subscriptionId);
|
||||
Assertions.assertEquals(2, entriesAfter.getEntries().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unstarringEntryMakesItEligibleForCleanup() {
|
||||
// Subscribe to feed and wait for entries
|
||||
Long subscriptionId = subscribeAndWaitForEntries(getFeedUrl());
|
||||
|
||||
// Star the first entry
|
||||
Entries entriesBefore = getFeedEntries(subscriptionId);
|
||||
Entry entry = entriesBefore.getEntries().getFirst();
|
||||
starEntry(entry.getId(), subscriptionId);
|
||||
|
||||
// Verify entry is starred
|
||||
Assertions.assertEquals(1, getCategoryEntries(CategoryREST.STARRED).getEntries().size());
|
||||
|
||||
// Unstar the entry
|
||||
unstarEntry(entry.getId(), subscriptionId);
|
||||
|
||||
// Verify entry is no longer starred
|
||||
Assertions.assertEquals(0, getCategoryEntries(CategoryREST.STARRED).getEntries().size());
|
||||
|
||||
// Run cleanup for entries older than now
|
||||
Instant olderThan = Instant.now().plus(Duration.ofDays(1));
|
||||
databaseCleaningService.cleanEntriesOlderThan(olderThan);
|
||||
|
||||
// Verify both entries were deleted (neither is starred)
|
||||
Entries entriesAfter = getFeedEntries(subscriptionId);
|
||||
Assertions.assertEquals(0, entriesAfter.getEntries().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
4
pom.xml
4
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>6.1.1</version>
|
||||
<version>6.2.0</version>
|
||||
<name>CommaFeed</name>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<version>3.15.0</version>
|
||||
<configuration>
|
||||
<parameters>true</parameters>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user