
Изваждане на backend-а от пътя на качването
GeekBye записва екрана ти и запазва видеото в твоя Google Drive. Първата версия прекарваше всеки запис през собствените сървъри на GeekBye по пътя натам; едно издание по-късно, файлът тръгваше директно от машината ти към Drive, а backend-ът беше понижен до това да държи един-единствен указател. Интересната част е колко малко код всъщност съдържа „директната, resumable" версия — защото resumability дойде от изтриването на един proxy, не от написването на такъв.
GeekBye може да записва екрана ти — видео, системно аудио и микрофон — и да пуска завършения запис в твоя Google Drive автоматично. Между v1.8.11 и v1.8.13 описанието на функцията, видимо за потребителя, почти не се промени, но пътят, който видеото изминава, за да стигне до Drive, беше пренасочен изцяло. В първата версия записът ти пътуваше през сървърите на GeekBye. Във втората тръгваше директно от машината ти към твоя Drive, а backend-ът на GeekBye никога не видя нито байт от него. Това пренасочване е цялата история, а удовлетворяващата част е, че „по-добрата" версия съдържа осезаемо по-малко код от тази, която замени.
Пайплайнът, от край до край
Записът се случва в renderer-а. ScreenRecordingService.ts създава MediaRecorder върху capture stream-а с { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } и го стартира с едносекунден timeslice (CHUNK_INTERVAL_MS = 1000). Всеки ondataavailable blob се изпраща през IPC към главния процес, където handler-ът recording:save-chunk го добавя към файл на диска с fs.promises.appendFile. Струва си да се отбележи рано, защото е нещо изкушаващо за погрешно тълкуване: тази едносекундна каденция е ритъмът на записа на диска, не размер на chunk за качване. Докато качването започне, на диска стои един-единствен завършен WebM файл.
Това, което се различава между двете издания, е само какво се случва с този завършен файл.
Преди: всичко през backend-а (v1.8.11)
Пътят на качване от v1.8.11, в CloudUploader.processRecording(), е очевидният дизайн и напълно разумна първа версия:
POST /api/geekbye/recordings, за да се създаде backend запис с метаданните на записа.- Прочитане на целия видеофайл в паметта и POST към backend-а като multipart —
const fileBuffer = fs.readFileSync(filePath)— удряйкиPOST /api/geekbye/recordings/${id}/upload. Backend-ът после качва този файл в Drive и връщаdriveUrl. POST …/process, за да се изпрати транскриптът за AI анализ.
Носещият детайл е стъпка 2: клиентът зарежда целия запис в паметта и всеки негов байт минава през собствените сървъри на GeekBye по пътя към Drive. За кратко клипче това е добре. За дълъг екранен запис това са три проблема наведнъж — натиск върху паметта на клиента от readFileSync, разход на трафик и препредаване на backend-а за файл, който той дори не пази, и един монолитен POST, който трябва да рестартира от нула, ако мрежата хлъцне по средата.
Струва си също да сме честни, че това „преди" не беше някакъв безупречен оригинален дизайн. Функцията в v1.8.11 сплеска в едно един пивот в рамките на седмицата: ранна версия качваше в Cloudflare R2 през pre-signed URL-и и конвертираше WebM в MP4 с вграден ffmpeg-static; това беше заменено от Drive flow, посредничен от backend-а; и после ffmpeg конверсията беше изкоренена изцяло в полза на качване на WebM-а както е (commit-ът отбелязва, че свали bitrate-а и произведе приблизително четири пъти по-малки файлове — вградена оценка, не benchmark). Така че дори „преди"-то вече беше научило веднъж да изтрие нативна зависимост. Следващото издание щеше да изтрие и proxy-то.
След: директно в Drive (v1.8.13)
v1.8.13 пренаписва CloudUploader.processRecording() около едно изречение от commit-а, който го пусна: качвай директно в Drive, „keeping backend for metadata record + AI transcript analysis only." Новият път:
POST /api/geekbye/recordings— само метаданни (заглавие, продължителност, размер на файла,format: 'webm'). Коментарът в кода е директен:// Create backend record (metadata only — no file upload).- Качи видеото директно в Google Drive, прескачайки напълно backend посредника.
PATCH /api/geekbye/recordings/${backendRecordingId}с получения{ driveUrl, driveFolderId }— на backend записа му се казва къде е свършил файлът.- Транскриптът все още отива към
…/processза анализ.
Backend-ът премина от това да бъде в пътя на файла към това да му се казва за файла постфактум. Той държи ред и указател; байтовете живеят само на диска на потребителя и в Drive на потребителя.
Да станеш истински Drive клиент
За да говори клиентът директно с Drive, той трябва да е реален Google Drive API клиент, и v1.8.13 добавя механиката за това: нов DriveService (Drive клиентът), нов DriveAuthRepository, подкрепен от нова SQLite таблица drive_auth, и SDK-а googleapis като зависимост.
Оторизацията е чисто предаване на способност. app-config endpoint-ът на backend-а препредава Google OAuth клиентски credentials — clientId и clientSecret — надолу към приложението, което CloudUploader чете и подава на driveService.initialize(...). Оттам desktop приложението изпълнява целия OAuth flow само: вдига временен http сървър на случаен 127.0.0.1 порт, отваря consent екрана в системния браузър с shell.openExternal, хваща redirect-а на /callback и разменя кода за token-и с google-auth-library. Тези token-и се запазват локално — saveDriveAuth(access_token, refresh_token, expiry_date) в drive_auth — със scope-а drive.file, който позволява на приложението да управлява само файловете, които създава. Обновяването също е локално: listener oauth2Client.on('tokens', …) записва обновените token-и директно обратно в таблицата. Backend-ът предава способността веднъж и после остава извън цикъла.
Частта „resumable", честно
Ето детайла, за който най-много искам да съм прям, защото е точно от онези неща, които changelog-ът заобля. Бележката към изданието казва „resumable uploads", и това е вярно от мястото, където стои потребителят. Но GeekBye не имплементира resumable протокола. Няма константа за размер на chunk, няма tracker на byte-offset и няма никаква обработка на Content-Range / 308 Resume Incomplete никъде в изходния код на GeekBye. Направи grep за тях и не получаваш нищо — те живеят вътре в SDK-а googleapis.
Това, което прави собственият код на GeekBye, е следното:
const fileStats = statSync(videoFilePath)
const videoStream = createReadStream(videoFilePath)
const videoRes = await this.drive.files.create(
{
requestBody: { name: `${title}.webm`, parents: [folderId] },
media: { mimeType: 'video/webm', body: videoStream },
fields: 'id',
},
{
onUploadProgress: (evt) => {
const percent = Math.round((evt.bytesRead / fileStats.size) * 100)
onProgress(percent)
},
},
)
Два избора вършат цялата работа. Първо, media.body е stream (createReadStream), не buffer — и подаването на stream е именно това, което кара googleapis да преговори resumable сесия от твое име, вместо да прави всичко наведнъж. Второ, прогресът се чете от callback-а onUploadProgress на SDK-а спрямо fileStats.size. Това е цялата имплементация на „resumable upload" на ниво приложение: stream навътре, прогрес навън. Трудният протокол — отвори сесия, качвай на chunk-ове, възобнови от offset след прекъсване — е работа на SDK-а, а правилното инженерно решение беше да го оставим да върши тази работа, вместо да я преоткриваме.
Което преформулира цялото издание. Подобрението в надеждността не дойде от написването на upload engine. То дойде от изтриването на proxy-то, което седеше пред такъв — и, по-рано същата седмица, от изтриването на ffmpeg стъпката пред него. По-малко код, повече устойчивост, защото устойчивото нещо беше добре изпробвана библиотека в момента, в който спреш да я храниш на ръка с buffer.
Парчетата, които са наистина на GeekBye
Предаването на трансфера на SDK-а не означава, че не остана нищо за строене. Две парчета реална приложна логика го обграждат.
Rollback за осиротели папки. Всеки запис получава своя собствена Drive подпапка (създадена с mimeType: 'application/vnd.google-apps.folder'), а транскриптът каца там като втори файл до видеото. Ако качването хвърли грешка, catch-ът изтрива тази папка — this.drive.files.delete({ fileId: folderId }), логнато като Rolled back orphaned Drive folder — така че провалено качване да не остави следа от празни папки в твоя Drive. Създаването на контейнер, преди да знаеш, че операцията ще успее, е малко задължение; направянето на пътя за провал да го почисти е поправката.
Жизнен цикъл около singleton. DriveService е singleton, който държи OAuth клиент в паметта, и този клиент в паметта не оцелява при рестарт на приложението, макар token-ите да оцеляват (те са в базата данни). Затова CloudUploader преинициализира DriveService от съхранените token-и преди всяко качване, което означава, че запис, направен веднага след повторно пускане, все още се качва, без да те моли да се свържеш наново. А review минаването превърна handler-а drive-connect във fire-and-forget, защото блокиращо connect извикване спираше status polling-а на renderer-а — UI-ят не можеше да покаже „connecting…", ако connect извикването държеше thread-а. Тези две — преинициализация-от-съхранение и неблокиращ connect — са реалните гранични случаи на директния път и, показателно, нито едно от тях не е за upload chunking. Когато делегираш трудната част, бъговете, с които оставаш, са бъгове на жизнения цикъл.
Три неща, на които ни научи това издание
- Питай накъде текат байтовете, не само дали работи. Proxy-то в v1.8.11 работеше. Но рутирането на всеки запис през собствените ти сървъри те струва трафик, памет и способност за рестартиране за файл, който не пазиш. Преначертаването на пътя на данните така, че клиентът да говори директно с дестинацията, често е цялата оптимизация.
- Най-доброто resumable качване е това, което не си написал. Подаването на stream към зрял SDK купи цяла resumable сесия за два реда код. Инстинктът да напишеш на ръка chunking-а и математиката на offset-а щеше да произведе повече код и по-малко надеждност.
- Когато делегираш ядрото, наследяваш жизнен цикъл, не алгоритми. Реалните бъгове на директния клиент бяха „преинициализирай singleton-а след рестарт" и „не блокирай connect извикването", не byte offset-и. Това е добра сделка — бъговете на жизнения цикъл са видими и локални; бъговете на протокола не са нито едното.
За предишната глава от историята на v1 — роден и поправен за дванадесет минути (v1.8.10); а за цялата дъга — анатомията на доставянето на софтуер до съвършенство.