-
Notifications
You must be signed in to change notification settings - Fork 70
Attached Libraries
מסד מצורף הוא קובץ SQLite בסכמת ספריית אוצריא (כמו seforim.db) שהמשתמש מצרף לתוכנה. הספרים שבו מקבלים את כל היכולות של ספרים רשמיים ואישיים, המסד נפתח לקריאה בלבד בתצורה מוקשחת, ואף טבלה אינה חובה.
תיעוד המשתמש ובוני המסדים: מסדי ספרים אישיים ו־docs/personal_databases.md.
BookSource (lib/models/book_source.dart) הוא מקור האמת לזהות הספר, ומחליף את isUserBook בכל השכבות:
| שכבה | ערכים |
|---|---|
BookSource |
official / user / attached(slug)
|
| מפתח wire (תוספים, JSON) |
o / u / d:<slug>
|
BookCompositeKey |
סיומת מקור על מפתח הספר |
| מפתח אינדקס החיפוש |
id: / uid: / ext: / db:<slug>:<id>
|
| הערות אישיות | "<title>|db:<slug>" |
bookUid לתוספים |
db:<slug>:<id> עם source: "attached"
|
-
ה־slug נלקח מ־
schema_meta.library_id, ואם אין — משם הקובץ אחרי ניקוי. אותיות (כולל עברית), ספרות,._-, עד 64 תווים. התנגשות slug ← הקובץ השני נדחה. - המפתחות של ספרים רשמיים ואישיים לא השתנו — סימניות, היסטוריה, אינדקס והגדרות פר־ספר שרדו את המעבר.
- ספרים מצורפים נוספים בסוף סדר הקטלוג של האינדקס, כך שצירוף מסד לא מזיז מפתחות של ספרים אחרים ולא מאנדקס אותם מחדש. החלפת קובץ באותו slug ← אינדוקס מחדש של אותו מסד בלבד.
lib/attached_libraries/
├── bloc/ # מצב המסדים בהגדרות (צירוף, הסרה, עדיפות, מיקום, הסתרה)
├── models/
├── repository/
│ ├── registry # פתיחה עצלה ומוקשחת, סגירה בזמן סרק, closeAll
│ ├── probe (isolate) # בדיקת קובץ לפני צירוף: slug, WAL/journal, טבלאות
│ ├── store # שמירת רשימת המסדים והגדרותיהם
│ ├── external_link_core / external_link_repository
│ └── update/ # מקור עדכונים, מניפסט, חתימה, רשת, התקנה
└── view/
lib/migration/database/
├── db_capabilities.dart # מקור אמת יחיד לקיום טבלאות/עמודות
└── untrusted_database.dart # openUntrustedReadOnlyDatabase, ReadOnlyDbTarget
-
DbCapabilities— רשימת היתר של טבלאות + getter יכולת לכל טבלה/זוג, במטמון לפי נתיב +PRAGMA schema_version. שם מוכר שהוא VIEW או טבלה וירטואלית נחשב חסר. זה חל גם עלseforim.dbהרשמי: טבלה חסרה משביתה רק את התכונה שתלויה בה. -
openUntrustedReadOnlyDatabase— read-only,query_only,trusted_schema=OFF, מצב defensive, ללא הרחבות, mmap כבוי.ReadOnlyDbTargetמעביר את אותה פתיחה ל־isolates. - קטלוג המסדים נקרא ב־isolate עם timeout לכל מסד; מסד שלא עונה מסומן "לא זמין" והעץ ממשיך להיבנות.
- איתור מקורות במסדים מצורפים רץ ב־isolate עובד ברקע.
טבלה אופציונלית שמאפשרת לספר במסד מצורף להצביע על ספר רשמי או על מסד אחר, לפי כותרת ו־heRef (לא FK), כדי לשרוד עדכוני ספרייה. ל־connectionType אותה משמעות כמו בטבלת link הרשמית — היעד כפי שהוא נראה מהמקור; ולכן פירוש שרוצה להופיע בחלונית המפרשים של ספר רשמי כותב connectionType='SOURCE'.
הקישורים ההפוכים מוגשים מאינדקס צד ב־cache.db, שנבנה מחדש כשקובץ המסד או גרסת הספרייה הרשמית משתנים. מגבלה: 5,000,000 שורות למסד (הבנייה בזרם, במנות), וחיתוך כותרת/הפניה ב־512 תווים.
user_book_version תמך עד כה רק בצמד אישי↔אישי. כעת הספר הראשי יכול להיות גם ספר רשמי או ספר במסד מצורף, לפי העמודות מקור_ראשי ו־קטגוריית_ראשי בקובץ גרסאות.csv. הראשי מצוין תמיד לפי כותרת כדי לשרוד עדכוני ספרייה; קבצים ישנים ממשיכים לעבוד בלי שינוי.
_runDeferredAttachedLibraries ב־main.dart רץ אחרי חשיפת החלון (שער reveal עם timeout), מוגן ב־WindowRole.isSecondary, ונכשל ללא הפלה דרך _logNonFatalInitializationError. שום דבר מהפיצ'ר לא נוסף למסלול שלפני הפריים הראשון — ראו רצף האתחול. כשאין מסדים מצורפים אין שום עלות בפתיחת ספר רשמי.
מפתחות db: נמחקים מהאינדקס רק כאשר:
- המסד הוסר מהרשימה, או
- הספר נעלם ממסד שנטען, גלוי ונגיש.
מסד לא זמין או מוסתר לעולם אינו גורם למחיקה — אחרת ניתוק כונן היה מוחק את האינדקס, הסימניות וההערות.
- הוסיפו את שם הטבלה לרשימת ההיתר ב־
DbCapabilities(וגם את בת הזוג, אם הן חייבות לבוא יחד). - הוסיפו getter יכולת —
hasXשבודק קיום ועמודות נדרשות. - שער בנקודת השאילתה — כל שאילתה נשמרת ב־getter ומחזירה ריק כשהיכולת חסרה. אין להניח קיום טבלה.
- עדכנו את
tool/validate_personal_db.dartואת קטלוג הטבלאות ב־docs/personal_databases.md.
טבלה שאינה ברשימת ההיתר לעולם לא נקראת — זה חלק ממודל האבטחה, לא מגבלה זמנית.
- המסד לעולם לא נכתב. עותק מנוהל (מובייל) הוא היחיד שה־journal שלו מנורמל.
- שום דבר מהמסד אינו מותקן או מורץ — תוספים, סקריפטים או גופנים. זו דרישה מפורשת: התקנת תוסף ממסד מיובא היא פריצת אבטחה.
- קישורים בטקסט הספר — ניווט בלבד.
-
book.filePath— יחסי לתיקיית המסד בלבד; נדחים נתיב מוחלט, אות כונן, UNC,.., רכיב של נקודות/רווחים בלבד,:ותו NUL.
המסד מצהיר ב־schema_meta על update_manifest_url (https) ועל update_public_key (ed25519, base64). AttachedLibraryProbe קורא אותם, ו־_applyProbe נועץ אותם ב־AttachedLibrary.updateSource בצירוף (TOFU). בבדיקה חוזרת של אותו slug הנעוץ נשמר, וכל סטייה — כולל הסרה של השדות — מסמנת updateSourceMismatch ללא שום פנייה לרשת. אין החלפת מפתח בגרסה הזו: מפתח שאבד מחייב הסרה וצירוף מחדש.
-
models/attached_update_manifest.dart— פענוח קפדני רק אחרי אימות החתימה.checkApplicableדוחהlibrary_idשונה ו־db_versionשאינו גדול מהמותקן. -
repository/update/attached_update_signature.dart— ed25519 דרךpinenacl(Dart טהור), תמיד מול המפתח הנעוץ ולעולם לא מול מפתח שהגיע בקובץ שהורד. - המניפסט מתאר ארטיפקט מלא: דחיסה, גודל, sha256 של ה־
.dbהסופי, ו־parts[]— כך שמסד גדול מ־2GB מפוצל לכמה חלקים ויכול לשבת על GitHub Releases או על כל שרת https אחר. לצדו יכולים לשבת תיקוני דלתא (ראו בהמשך).
AttachedUpdateHostPolicy דורשת https, שם מארח ולא IP, בלי שמות מקומיים, וכל כתובות ה־DNS ציבוריות. AttachedUpdateFetcher מתחבר דרך connectionFactory לכתובת שהמדיניות כבר אישרה ומקים TLS מול שם המארח (אין פענוח DNS שני — חסימת rebinding), מטפל בהפניות ידנית ובודק כל אחת, ומחיל timeouts על החיבור ועל כל נתח. תעודות נטפרי מגיעות מה־SecurityContext הגלובלי. dart:io בלבד, כדי שיוכל לרוץ ב־isolate.
מגבלה מתועדת: מאחורי פרוקסי HTTP, הפרוקסי הוא שמפענח את השם, ולכן חסימת הכתובות הפרטיות נשענת על פענוח מקומי בלבד.
_runDeferredAttachedLibraryUpdates() רץ אחרי סריקת המסדים, בחלון הראשי בלבד, מאחורי אותם שערים כמו בדיקת הספרייה הרשמית: לא במצב לא מקוון, keySoftwareAndBookUpdatesEnabled, keyAutoSync, ותדירות keyUpdateCheckFrequency. מסד בלי מקור נעוץ או עם updateSourceMismatch לא נבדק כלל.
-
AttachedUpdateDownloaderמריץ הורדה, sha256 לכל חלק, פריסת zstd בזרם (ZstdStreamExtractor, אותו FFI של עדכון הספרייה) ו־sha256 סופי — הכל ב־isolate אחד, עם ביטול דרך פורט בקרה והמשכיות ב־Range אחרי כשל רשת. - לפני ההורדה נבדקת כתיבוּת תיקיית היעד ומקום פנוי בכל כונן (מרווח 32MB).
-
installUpdateרץ בתור הסדרתי של המאגר:registry.close(slug)משחרר נעילות,AttachedUpdateFileSwap.swapInמעביר את הקובץ הישן וקבצי הצד שלו ל־<file>.bak-updateומכניס את החדש. הקובץ החדש נבדק פעמיים — לפני ההחלפה ואחריה: אותוlibrary_id, אותו מפתח, ו־db_versionהשווה בדיוק למניפסט. כשל ⇒ שחזור הגיבוי. -
קריסה באמצע:
recoverInterruptedUpdates()רץ לפני ה־rescan—.bak-updateקיים ⇒ מוחזר למקום, וקובץ.update-newיתום נמחק. - אחרי הצלחה, טביעת האצבע החדשה מפעילה את מסלול הרענון הקיים: עץ, אינדוקס של אותו מקור בלבד, אינדקס
external_linkומטמונים.
dart run tool/personal_db_update.dart keygen|pack|sign|verify — יצירת מפתח, דחיסה ופיצול לחלקים עם sha256, חתימה ואימות.
הדלתא כאן היא ברמת הבתים — פלט של zstd --patch-from — ולא patch ברמת השורה כמו בספרייה הרשמית. זו הייתה ההכרעה המרכזית: ה־PatchApplier של seforim_library_updater קשור לרשימת הטבלאות ולמפתחות הראשיים של הסכמה הרשמית, ל־LogicalContentHasher שמחושב על סדר טבלאות קפוא, ולקובצי patch שמיוצרים בצינור ה־Kotlin של SeforimLibrary — למפרסם פרטי אין דרך לייצר אותם, ובמסד שנבנה מחדש בכל גרסה גם ממילא אין זהות שורה יציבה שתעשה את ה־patch קטן.
-
מניפסט: ערך ב־
delta[]עםcompression: "zstd-patch", from_db_versionו־from_sha256. ה־sizeוה־sha256הם של ה־.dbהמוגמר — זהים לאלה שלfull, ולכן הערובה הסופית אינה משתנה. -
סובלנות: ערך דלתא שאינו נפרש מדולג, ולא מפיל את המניפסט כולו. בלי זה, מפרסם שמוסיף דלתא היה משתיק את העדכונים אצל כל לקוח ישן יותר.
fullנשאר קפדני. -
בחירה (
AttachedUpdateArtifactPlanner): דלתא נבחרת רק כש־from_db_versionתואם, ה־sha256 של הקובץ המותקן שווה ל־from_sha256, ההורדה קטנה מזו שלfull, המערכת 64־ביט והקובץ המותקן עד 2GiB. מבין המתאימות נבחרת הקטנה. ה־sha של הקובץ המותקן מחושב ב־isolate רק כשקיימת מועמדת תואמת־גרסה, ונשמר במטמון לפי (נתיב, גודל, mtime). -
החלה:
MappedFileממפה את הקובץ המותקן לקריאה בלבד, ו־ZSTD_DCtx_refPrefixמקבל את המיפוי כ־prefix עםwindowLogMax=31. המיפוי אינו מתיר שיתוף לכתיבה — קיצור הקובץ תוך כדי מיפוי הוא תקלת עמוד שאי אפשר לתפוס ב־Dart והיא מפילה את התהליך, ולכן עדיף שהמיפוי ייכשל והעדכון ירד במלואו. -
נפילה חזרה: כשל בתיקון עצמו (פענוח, חריגה מהגודל, sha) מוריד את
fullבאותה ריצה, בלי שגיאה למשתמש. כשל רשת אינו מפיל חזרה — הוא מדווח פעם אחת, וההורדה החלקית נשמרת להמשך. -
ביטול: הפענוח סינכרוני בתוך
Isolate.run, ולכן הודעה ב־SendPortהייתה מגיעה רק אחרי שהוא הסתיים. הביטול עובר בתא זיכרון נייטיב (ZstdCancelFlag) שהלולאה בודקת פעם לכל מנת פלט. -
כלי למפרסם:
pack --delta-from <old.db>(עד 4), ו־verifyשמחיל כל תיקון דרך אותו applier שהלקוח מריץ.
📖 מדריך למשתמש
- התחלת העבודה
- הספרייה
- קריאת ספר
- קריאת PDF
- חיפוש
- איתור מקורות
- טאבים ושולחנות
- סימניות והערות
- ספרים אישיים
- מסדי ספרים אישיים
- הגדרות
- גיבוי ושחזור
- עדכונים
- תוספים
- כלים
- קישורי עומק
- פתרון תקלות
🛠️ למפתחים — יסודות
נתונים וספרייה
קריאה
חיפוש
אישי ומערכת
עיצוב ו־UI