ソースを参照

Web export SQLite (#6931)

* Web export  SQLite
https://github.com/FreshRSS/FreshRSS/discussions/6930

* Implement download

* Fix operator precedence

* Set Last-Modified

* Sort by time, newest first

* Fix Last-Modified

* Use DateTimeInterface::RFC7231

* Add not_applicable message
Alexandre Alapetite 1 年間 前
コミット
7a5ce0fe20
52 ファイル変更204 行追加29 行削除
  1. 38 0
      app/Controllers/importExportController.php
  2. 3 0
      app/Models/View.php
  3. 1 0
      app/i18n/cs/gen.php
  4. 4 1
      app/i18n/cs/sub.php
  5. 1 0
      app/i18n/de/gen.php
  6. 4 1
      app/i18n/de/sub.php
  7. 1 0
      app/i18n/el/gen.php
  8. 4 1
      app/i18n/el/sub.php
  9. 1 0
      app/i18n/en-us/gen.php
  10. 4 1
      app/i18n/en-us/sub.php
  11. 5 4
      app/i18n/en/gen.php
  12. 4 1
      app/i18n/en/sub.php
  13. 1 0
      app/i18n/es/gen.php
  14. 4 1
      app/i18n/es/sub.php
  15. 1 0
      app/i18n/fa/gen.php
  16. 4 1
      app/i18n/fa/sub.php
  17. 1 0
      app/i18n/fr/gen.php
  18. 4 1
      app/i18n/fr/sub.php
  19. 1 0
      app/i18n/he/gen.php
  20. 4 1
      app/i18n/he/sub.php
  21. 1 0
      app/i18n/hu/gen.php
  22. 4 1
      app/i18n/hu/sub.php
  23. 1 0
      app/i18n/id/gen.php
  24. 4 1
      app/i18n/id/sub.php
  25. 1 0
      app/i18n/it/gen.php
  26. 4 1
      app/i18n/it/sub.php
  27. 1 0
      app/i18n/ja/gen.php
  28. 4 1
      app/i18n/ja/sub.php
  29. 1 0
      app/i18n/ko/gen.php
  30. 4 1
      app/i18n/ko/sub.php
  31. 1 0
      app/i18n/lv/gen.php
  32. 4 1
      app/i18n/lv/sub.php
  33. 1 0
      app/i18n/nl/gen.php
  34. 4 1
      app/i18n/nl/sub.php
  35. 1 0
      app/i18n/oc/gen.php
  36. 4 1
      app/i18n/oc/sub.php
  37. 1 0
      app/i18n/pl/gen.php
  38. 4 1
      app/i18n/pl/sub.php
  39. 1 0
      app/i18n/pt-br/gen.php
  40. 4 1
      app/i18n/pt-br/sub.php
  41. 1 0
      app/i18n/ru/gen.php
  42. 4 1
      app/i18n/ru/sub.php
  43. 1 0
      app/i18n/sk/gen.php
  44. 4 1
      app/i18n/sk/sub.php
  45. 1 0
      app/i18n/tr/gen.php
  46. 4 1
      app/i18n/tr/sub.php
  47. 1 0
      app/i18n/zh-cn/gen.php
  48. 4 1
      app/i18n/zh-cn/sub.php
  49. 1 0
      app/i18n/zh-tw/gen.php
  50. 4 1
      app/i18n/zh-tw/sub.php
  51. 30 1
      app/views/importExport/index.phtml
  52. 9 0
      app/views/importExport/sqlite.phtml

+ 38 - 0
app/Controllers/importExportController.php

@@ -31,6 +31,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 	public function indexAction(): void {
 		$this->view->feeds = $this->feedDAO->listFeeds();
 		FreshRSS_View::prependTitle(_t('sub.import_export.title') . ' · ');
+		$this->listSqliteArchives();
 	}
 
 	private static function megabytes(string $size_str): float|int|string {
@@ -694,4 +695,41 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 				return 'application/octet-stream';
 		}
 	}
+
+	private const REGEX_SQLITE_FILENAME = '/^(?![.-])[0-9a-zA-Z_.@ #&()~\-]{1,128}\.sqlite$/';
+
+	private function listSqliteArchives(): void {
+		$this->view->sqliteArchives = [];
+		$files = glob(USERS_PATH . '/' . Minz_User::name() . '/*.sqlite', GLOB_NOSORT) ?: [];
+		foreach ($files as $file) {
+			$archive = [
+				'name' => basename($file),
+				'size' => @filesize($file),
+				'mtime' => @filemtime($file),
+			];
+			if ($archive['size'] != false && $archive['mtime'] != false && preg_match(self::REGEX_SQLITE_FILENAME, $archive['name'])) {
+				$this->view->sqliteArchives[] = $archive;
+			}
+		}
+		// Sort by time, newest first:
+		usort($this->view->sqliteArchives, static fn(array $a, array $b): int => $b['mtime'] <=> $a['mtime']);
+	}
+
+	public function sqliteAction(): void {
+		if (!Minz_Request::isPost()) {
+			Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true);
+		}
+		$sqlite = Minz_Request::paramString('sqlite');
+		if (!preg_match(self::REGEX_SQLITE_FILENAME, $sqlite)) {
+			Minz_Error::error(404);
+			return;
+		}
+		$path = USERS_PATH . '/' . Minz_User::name() . '/' . $sqlite;
+		if (!file_exists($path) || @filesize($path) == false || @filemtime($path) == false) {
+			Minz_Error::error(404);
+			return;
+		}
+		$this->view->sqlitePath = $path;
+		$this->view->_layout(null);
+	}
 }

+ 3 - 0
app/Models/View.php

@@ -82,6 +82,9 @@ class FreshRSS_View extends Minz_View {
 	public string $list_title;
 	public int $queryId;
 	public string $type;
+	/** @var null|array<array{name:string,size:int,mtime:int}> */
+	public ?array $sqliteArchives = null;
+	public string $sqlitePath;
 
 	// Form login
 	public int $cookie_days;

+ 1 - 0
app/i18n/cs/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Odstranění ztlumených zdrojů',
 		'demote' => 'Snížit úroveň',
 		'disable' => 'Zakázat',
+		'download' => 'Download',	// TODO
 		'empty' => 'Vyprázdnit',
 		'enable' => 'Povolit',
 		'export' => 'Exportovat',

+ 4 - 1
app/i18n/cs/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Okamžité oznámení s WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Exportovat',
+		'export' => array(
+			'_' => 'Exportovat',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Exportovat články s vašimi popisky',
 		'export_opml' => 'Exportovat seznam kanálů (OPML)',
 		'export_starred' => 'Exportovat vaše oblíbené',

+ 1 - 0
app/i18n/de/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Lösche stumm gestellte Feeds',
 		'demote' => 'Zurückstufen',
 		'disable' => 'Deaktivieren',
+		'download' => 'Download',	// TODO
 		'empty' => 'Leeren',
 		'enable' => 'Aktivieren',
 		'export' => 'Exportieren',

+ 4 - 1
app/i18n/de/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Sofortbenachrichtigung mit WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Exportieren',
+		'export' => array(
+			'_' => 'Exportieren',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Artikel mit Labeln exportieren',
 		'export_opml' => 'Liste der Feeds exportieren (OPML)',
 		'export_starred' => 'Ihre Favoriten exportieren',

+ 1 - 0
app/i18n/el/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Delete muted feeds',	// TODO
 		'demote' => 'Demote',	// TODO
 		'disable' => 'Disable',	// TODO
+		'download' => 'Download',	// TODO
 		'empty' => 'Empty',	// TODO
 		'enable' => 'Enable',	// TODO
 		'export' => 'Export',	// TODO

+ 4 - 1
app/i18n/el/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Instant notifications with WebSub',	// TODO
 	),
 	'import_export' => array(
-		'export' => 'Export',	// TODO
+		'export' => array(
+			'_' => 'Export',	// TODO
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Export your labelled articles',	// TODO
 		'export_opml' => 'Export list of feeds (OPML)',	// TODO
 		'export_starred' => 'Export your favourites',	// TODO

+ 1 - 0
app/i18n/en-us/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Delete muted feeds',	// IGNORE
 		'demote' => 'Demote',	// IGNORE
 		'disable' => 'Disable',	// IGNORE
+		'download' => 'Download',	// IGNORE
 		'empty' => 'Empty',	// IGNORE
 		'enable' => 'Enable',	// IGNORE
 		'export' => 'Export',	// IGNORE

+ 4 - 1
app/i18n/en-us/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Instant notifications with WebSub',	// IGNORE
 	),
 	'import_export' => array(
-		'export' => 'Export',	// IGNORE
+		'export' => array(
+			'_' => 'Export',	// IGNORE
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Export your labeled articles',
 		'export_opml' => 'Export list of feeds (OPML)',	// IGNORE
 		'export_starred' => 'Export your favorites',

+ 5 - 4
app/i18n/en/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Delete muted feeds',
 		'demote' => 'Demote',
 		'disable' => 'Disable',
+		'download' => 'Download',
 		'empty' => 'Empty',
 		'enable' => 'Enable',
 		'export' => 'Export',
@@ -33,9 +34,9 @@ return array(
 			'open' => 'Open menu',
 		),
 		'nav_buttons' => array(
-			'next' => 'Next article',	// TODO
-			'prev' => 'Previous article',	// TODO
-			'up' => 'Go up',	// TODO
+			'next' => 'Next article',
+			'prev' => 'Previous article',
+			'up' => 'Go up',
 		),
 		'open_url' => 'Open URL',
 		'promote' => 'Promote',
@@ -183,7 +184,7 @@ return array(
 		'display' => 'Display',
 		'extensions' => 'Extensions',
 		'logs' => 'Logs',
-		'privacy' => 'Privacy',	// TODO
+		'privacy' => 'Privacy',
 		'queries' => 'User queries',
 		'reading' => 'Reading',
 		'search' => 'Search words or #tags',

+ 4 - 1
app/i18n/en/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Instant notifications with WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Export',
+		'export' => array(
+			'_' => 'Export',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Export your labelled articles',
 		'export_opml' => 'Export list of feeds (OPML)',
 		'export_starred' => 'Export your favourites',

+ 1 - 0
app/i18n/es/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Eliminar fuentes silenciadas',
 		'demote' => 'Degradar',
 		'disable' => 'Desactivar',
+		'download' => 'Download',	// TODO
 		'empty' => 'Vaciar',
 		'enable' => 'Activar',
 		'export' => 'Exportar',

+ 4 - 1
app/i18n/es/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Notificación inmediata con WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Exportar',
+		'export' => array(
+			'_' => 'Exportar',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Exporta tus artículos etiquetados',
 		'export_opml' => 'Exportar la lista de fuentes (OPML)',
 		'export_starred' => 'Exportar tus favoritos',

+ 1 - 0
app/i18n/fa/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => ' فیدهای خاموش را حذف کنید',
 		'demote' => ' تنزل دادن',
 		'disable' => ' غیر فعال کردن',
+		'download' => 'Download',	// TODO
 		'empty' => ' خالی',
 		'enable' => ' فعال کنید',
 		'export' => ' صادرات',

+ 4 - 1
app/i18n/fa/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => ' اطلاع رسانی فوری با WebSub',
 	),
 	'import_export' => array(
-		'export' => ' صادرات',
+		'export' => array(
+			'_' => ' صادرات',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => ' مقالات برچسب دار خود را صادر کنید',
 		'export_opml' => ' لیست صادرات فیدها (OPML)',
 		'export_starred' => ' موارد دلخواه خود را صادر کنید',

+ 1 - 0
app/i18n/fr/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Supprimer les flux désactivés',
 		'demote' => 'Rétrograder',
 		'disable' => 'Désactiver',
+		'download' => 'Télécharger',
 		'empty' => 'Vider',
 		'enable' => 'Activer',
 		'export' => 'Exporter',

+ 4 - 1
app/i18n/fr/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Notifications instantanée par WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Exporter',
+		'export' => array(
+			'_' => 'Exporter',
+			'sqlite' => 'Télécharger la base de donnée de l’utilisateur au format SQLite',
+		),
 		'export_labelled' => 'Exporter les articles étiquetés',
 		'export_opml' => 'Exporter la liste des flux (OPML)',
 		'export_starred' => 'Exporter les favoris',

+ 1 - 0
app/i18n/he/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Delete muted feeds',	// TODO
 		'demote' => 'Demote',	// TODO
 		'disable' => 'Disable',	// TODO
+		'download' => 'Download',	// TODO
 		'empty' => 'Empty',	// TODO
 		'enable' => 'Enable',	// TODO
 		'export' => 'ייצוא',

+ 4 - 1
app/i18n/he/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Instant notifications with WebSub',	// TODO
 	),
 	'import_export' => array(
-		'export' => 'ייצוא',
+		'export' => array(
+			'_' => 'ייצוא',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Export your labelled articles',	// TODO
 		'export_opml' => 'ייצוא רשימת הזנות (OPML)',
 		'export_starred' => 'ייצוא מועדפים',

+ 1 - 0
app/i18n/hu/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Némított hírforrások törlése',
 		'demote' => 'Lefokoz',
 		'disable' => 'Kikapcsol',
+		'download' => 'Download',	// TODO
 		'empty' => 'Üres',
 		'enable' => 'Bekapcsol',
 		'export' => 'Export',	// IGNORE

+ 4 - 1
app/i18n/hu/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Azonnali értesítés WebSub-al',
 	),
 	'import_export' => array(
-		'export' => 'Exportálás',
+		'export' => array(
+			'_' => 'Exportálás',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Címkézett cikkek exportálása',
 		'export_opml' => 'Hírforrások listájának exportálása (OPML)',
 		'export_starred' => 'Kedvencek exportálása',

+ 1 - 0
app/i18n/id/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Delete muted feeds',	// TODO
 		'demote' => 'Demote',	// TODO
 		'disable' => 'Disable',	// TODO
+		'download' => 'Download',	// TODO
 		'empty' => 'Empty',	// TODO
 		'enable' => 'Enable',	// TODO
 		'export' => 'Export',	// TODO

+ 4 - 1
app/i18n/id/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Instant notifications with WebSub',	// TODO
 	),
 	'import_export' => array(
-		'export' => 'Export',	// TODO
+		'export' => array(
+			'_' => 'Export',	// TODO
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Export your labeled articles',
 		'export_opml' => 'Export list of feeds (OPML)',	// TODO
 		'export_starred' => 'Export your favorites',

+ 1 - 0
app/i18n/it/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Cancella i feed mutati',
 		'demote' => 'Retrocedi',
 		'disable' => 'Disabilita',
+		'download' => 'Download',	// TODO
 		'empty' => 'Vuoto',
 		'enable' => 'Abilita',
 		'export' => 'Esporta',

+ 4 - 1
app/i18n/it/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Notifica istantanea con WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Esporta',
+		'export' => array(
+			'_' => 'Esporta',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Esporta gli articoli etichettati',
 		'export_opml' => 'Esporta tutta la lista dei feed (OPML)',
 		'export_starred' => 'Esporta i tuoi preferiti',

+ 1 - 0
app/i18n/ja/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'ミュートにしているフィードを削除する',
 		'demote' => '寄付',
 		'disable' => '無効',
+		'download' => 'Download',	// TODO
 		'empty' => '空',
 		'enable' => '有効',
 		'export' => 'エクスポート',

+ 4 - 1
app/i18n/ja/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'WebSubとの即時通知',
 	),
 	'import_export' => array(
-		'export' => 'エクスポート',
+		'export' => array(
+			'_' => 'エクスポート',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'ラベル付けされた記事をエクスポートする',
 		'export_opml' => 'フィードリストをエクスポートする (OPML)',
 		'export_starred' => 'お気に入りをエクスポートする',

+ 1 - 0
app/i18n/ko/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => '음소거된 피드 삭제',
 		'demote' => '목록 수준 내리기',
 		'disable' => '비활성화',
+		'download' => 'Download',	// TODO
 		'empty' => '비우기',
 		'enable' => '활성화',
 		'export' => '내보내기',

+ 4 - 1
app/i18n/ko/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'WebSub을 사용한 즉시 알림',
 	),
 	'import_export' => array(
-		'export' => '내보내기',
+		'export' => array(
+			'_' => '내보내기',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => '라벨이 표시된 글들 내보내기',
 		'export_opml' => '피드 목록 내보내기 (OPML)',
 		'export_starred' => '즐겨찾기 내보내기',

+ 1 - 0
app/i18n/lv/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Izdzēst izslēgtās barotnes',
 		'demote' => 'Pazemināt amatu',
 		'disable' => 'Izslēgt',
+		'download' => 'Download',	// TODO
 		'empty' => 'Iztukšot',
 		'enable' => 'Ieslēgt',
 		'export' => 'Eksportēt',

+ 4 - 1
app/i18n/lv/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Tūlītēji paziņojumi ar WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Eksportēt',
+		'export' => array(
+			'_' => 'Eksportēt',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Eksportēt ar birku marķētus rakstus',
 		'export_opml' => 'Eksportēt barotņu sarakstu (OPML)',
 		'export_starred' => 'Eksportēt mīļākos',

+ 1 - 0
app/i18n/nl/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Gedempte feeds verwijderen',
 		'demote' => 'Degraderen',
 		'disable' => 'Uitzetten',
+		'download' => 'Download',	// TODO
 		'empty' => 'Leeg',
 		'enable' => 'Aanzetten',
 		'export' => 'Exporteren',

+ 4 - 1
app/i18n/nl/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Directe notificaties met WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Exporteer',
+		'export' => array(
+			'_' => 'Exporteer',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Exporteer gelabelde artikels',
 		'export_opml' => 'Exporteer lijst van feeds (OPML)',
 		'export_starred' => 'Exporteer je favorieten',

+ 1 - 0
app/i18n/oc/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Suprimir los flux muts',
 		'demote' => 'Retrogradar',
 		'disable' => 'Desactivar',
+		'download' => 'Download',	// TODO
 		'empty' => 'Voidar',
 		'enable' => 'Activar',
 		'export' => 'Exportar',

+ 4 - 1
app/i18n/oc/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Notificacions instantanèas amb WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Exportar',
+		'export' => array(
+			'_' => 'Exportar',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Exportar los articles etiquetats',
 		'export_opml' => 'Exportar la lista de fluxes (OPML)',
 		'export_starred' => 'Exportar los favorits',

+ 1 - 0
app/i18n/pl/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Usuń wyciszone kanały',
 		'demote' => 'Zdegraduj',
 		'disable' => 'Wyłącz',
+		'download' => 'Download',	// TODO
 		'empty' => 'Opróżnij',
 		'enable' => 'Włącz',
 		'export' => 'Eksportuj',

+ 4 - 1
app/i18n/pl/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Natychmiastowe powiadomienia protokołu WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Eksport',
+		'export' => array(
+			'_' => 'Eksport',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Eksportuj wiadomości z etykietami',
 		'export_opml' => 'Eksportuj listę kanałów (format OPML)',
 		'export_starred' => 'Eksportuj ulubione wiadomości',

+ 1 - 0
app/i18n/pt-br/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Excluir feeds silenciados',
 		'demote' => 'Despromover',
 		'disable' => 'Desabilitar',
+		'download' => 'Download',	// TODO
 		'empty' => 'Vazio',
 		'enable' => 'Habilitar',
 		'export' => 'Exportar',

+ 4 - 1
app/i18n/pt-br/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Notificação instantânea com WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Exportar',
+		'export' => array(
+			'_' => 'Exportar',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Exportar seus artigos etiquetados',
 		'export_opml' => 'Exporta a lista dos feeds (OPML)',
 		'export_starred' => 'Exportar seus favoritos',

+ 1 - 0
app/i18n/ru/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Удалить заглушенные ленты',
 		'demote' => 'Понизить',
 		'disable' => 'Отключить',
+		'download' => 'Download',	// TODO
 		'empty' => 'Опустошить',
 		'enable' => 'Включить',
 		'export' => 'Экспортировать',

+ 4 - 1
app/i18n/ru/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Моментальные оповещения посредством WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Экспорт',
+		'export' => array(
+			'_' => 'Экспорт',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Экспортировать ваши помеченные статьи',
 		'export_opml' => 'Экспортировать список лент (OPML)',
 		'export_starred' => 'Экспортировать ваше избранное',

+ 1 - 0
app/i18n/sk/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Vymazať stíšené kanály',
 		'demote' => 'Degradovať',
 		'disable' => 'Zakázať',
+		'download' => 'Download',	// TODO
 		'empty' => 'Vyprázdniť',
 		'enable' => 'Povoliť',
 		'export' => 'Exportovať',

+ 4 - 1
app/i18n/sk/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'Okamžité oznámenia cez WebSub',
 	),
 	'import_export' => array(
-		'export' => 'Exportovať',
+		'export' => array(
+			'_' => 'Exportovať',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Exportovať vaše označené články',
 		'export_opml' => 'Exportovať zoznam kanálov (OPML)',
 		'export_starred' => 'Exportovať vaše obľúbené',

+ 1 - 0
app/i18n/tr/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => 'Sessize alınmış akışları sil',
 		'demote' => 'Yöneticilikten al',
 		'disable' => 'Pasif',
+		'download' => 'Download',	// TODO
 		'empty' => 'Boş',
 		'enable' => 'Aktif',
 		'export' => 'Dışa Aktar',

+ 4 - 1
app/i18n/tr/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'WebSub ile anlık bildirim',
 	),
 	'import_export' => array(
-		'export' => 'Dışa aktar',
+		'export' => array(
+			'_' => 'Dışa aktar',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => 'Etiketli makaleleri dışarı aktar',
 		'export_opml' => 'Akış listesini dışarı aktar (OPML)',
 		'export_starred' => 'Favorileri dışarı aktar',

+ 1 - 0
app/i18n/zh-cn/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => '删除已暂停的订阅源',
 		'demote' => '撤销管理员',
 		'disable' => '禁用',
+		'download' => 'Download',	// TODO
 		'empty' => '清空',
 		'enable' => '启用',
 		'export' => '导出',

+ 4 - 1
app/i18n/zh-cn/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'WebSub 即时通知',
 	),
 	'import_export' => array(
-		'export' => '导出',
+		'export' => array(
+			'_' => '导出',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => '导出有标签的文章',
 		'export_opml' => '导出订阅源列表(OPML)',
 		'export_starred' => '导出你的收藏',

+ 1 - 0
app/i18n/zh-tw/gen.php

@@ -21,6 +21,7 @@ return array(
 		'delete_muted_feeds' => '刪除已暫停的訂閱源',
 		'demote' => '撤銷管理員',
 		'disable' => '禁用',
+		'download' => 'Download',	// TODO
 		'empty' => '清空',
 		'enable' => '啟用',
 		'export' => '導出',

+ 4 - 1
app/i18n/zh-tw/sub.php

@@ -246,7 +246,10 @@ return array(
 		'websub' => 'WebSub 即時通知',
 	),
 	'import_export' => array(
-		'export' => '導出',
+		'export' => array(
+			'_' => '導出',
+			'sqlite' => 'Download user database as SQLite',	// TODO
+		),
 		'export_labelled' => '導出有標籤的文章',
 		'export_opml' => '導出訂閱源列表(OPML)',
 		'export_starred' => '導出你的收藏',

+ 30 - 1
app/views/importExport/index.phtml

@@ -65,7 +65,7 @@
 						$select_args = ' size="' . min(10, count($this->feeds)) . '" multiple="multiple"';
 					}
 				?>
-				<select name="export_feeds[]"<?= $select_args ?> size="10">
+				<select name="export_feeds[]"<?= $select_args ?>>
 					<?= extension_loaded('zip') ? '' : '<option></option>' ?>
 					<?php foreach ($this->feeds as $feed) { ?>
 					<option value="<?= $feed->id() ?>"><?= $feed->name() ?></option>
@@ -81,4 +81,33 @@
 		</div>
 	</form>
 	<?php } ?>
+
+	<h2><?= _t('sub.import_export.export.sqlite') ?></h2>
+	<?php if (count($this->sqliteArchives ?? []) === 0): ?>
+	<p class="alert alert-warn">
+		<?= _t('gen.short.not_applicable') ?>
+	</p>
+	<?php else: ?>
+	<form method="post" action="<?= _url('importExport', 'sqlite') ?>">
+		<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
+		<div class="form-group">
+			<div class="group-controls">
+				<select name="sqlite">
+					<?php foreach ($this->sqliteArchives ?? [] as $sqliteArchive): ?>
+					<option value="<?= htmlspecialchars($sqliteArchive['name'], ENT_COMPAT, 'UTF-8') ?>">
+						<?= htmlspecialchars($sqliteArchive['name'], ENT_NOQUOTES, 'UTF-8') ?>
+						<small>(<?= format_bytes($sqliteArchive['size']) ?> · <?= date('c', $sqliteArchive['mtime']) ?>)</small>
+					</option>
+					<?php endforeach; ?>
+				</select>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?= _t('gen.action.download') ?></button>
+			</div>
+		</div>
+	</form>
+	<?php endif; ?>
 </main>

+ 9 - 0
app/views/importExport/sqlite.phtml

@@ -0,0 +1,9 @@
+<?php
+declare(strict_types=1);
+/** @var FreshRSS_View $this */
+header('Content-Type: application/vnd.sqlite3');
+header('Content-Disposition: attachment; filename="' . basename($this->sqlitePath) . '"');
+header('Cache-Control: private, no-store, max-age=0');
+header('Last-Modified: ' . gmdate(DateTimeInterface::RFC7231, @filemtime($this->sqlitePath) ?: 0));
+header('Content-Length: ' . (@filesize($this->sqlitePath) ?: 0));
+readfile($this->sqlitePath);