
Убираем backend с пути загрузки
GeekBye записывает твой экран и сохраняет видео в твой Google Drive. Первая версия прогоняла каждую запись через собственные серверы GeekBye по пути туда; релизом позже файл шёл прямо с твоей машины на Drive, а backend был понижен до хранения единственного указателя. Интересная часть в том, как мало кода на самом деле содержит «прямая, возобновляемая» версия — потому что возобновляемость пришла из удаления proxy, а не из его написания.
GeekBye умеет записывать твой экран — видео, системный звук и микрофон — и автоматически складывать готовую запись в твой Google Drive. Между v1.8.11 и v1.8.13 обращённое к пользователю описание функции почти не изменилось, но путь, которым видео добирается до Drive, был переложен полностью. В первой версии твоя запись путешествовала через серверы GeekBye. Во второй она шла прямо с твоей машины на твой Drive, и backend GeekBye ни разу не видел ни одного её байта. Эта переправа — вся история, и приятная часть в том, что «лучшая» версия содержит заметно меньше кода, чем та, которую она заменила.
Конвейер, от начала до конца
Запись происходит в рендерере. ScreenRecordingService.ts создаёт MediaRecorder поверх потока захвата с { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } и запускает его с односекундным срезом времени (CHUNK_INTERVAL_MS = 1000). Каждый blob ondataavailable отправляется через IPC в главный процесс, где обработчик recording:save-chunk дописывает его в файл на диске с помощью fs.promises.appendFile. Стоит отметить сразу, потому что это соблазнительно прочитать неверно: этот односекундный ритм — ритм записи на диск, а не размер фрагмента загрузки. К моменту, когда загрузка начинается, на диске лежит единственный готовый файл 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 через предподписанные URL и конвертировала WebM в MP4 встроенным ffmpeg-static; это было заменено потоком Drive через backend; а потом ffmpeg-конвертацию вырвали полностью в пользу выгрузки WebM как есть (коммит отмечает, что это снизило битрейт и дало примерно вчетверо меньшие файлы — прикидка на глаз, не бенчмарк). Так что даже «как было» уже однажды научилось удалять нативную зависимость. Следующий релиз удалил бы и proxy.
Как стало: прямо на Drive (v1.8.13)
v1.8.13 переписывает CloudUploader.processRecording() вокруг одного предложения из коммита, что его выпустил: выгружай прямо на Drive, «оставляя backend только для записи метаданных + AI-анализа транскрипта.» Новый путь:
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 как зависимость.
Авторизация — это чистая передача полномочия. Эндпоинт конфигурации приложения на backend передаёт клиентские учётные данные Google OAuth — clientId и clientSecret — вниз в приложение, которые CloudUploader читает и передаёт в driveService.initialize(...). Оттуда десктопное приложение само проводит весь поток OAuth: поднимает временный http-сервер на случайном порту 127.0.0.1, открывает экран согласия в системном браузере через shell.openExternal, ловит редирект на /callback и меняет код на токены с помощью google-auth-library. Эти токены сохраняются локально — saveDriveAuth(access_token, refresh_token, expiry_date) в drive_auth — с областью drive.file, что позволяет приложению управлять только теми файлами, которые оно создаёт. Обновление тоже локальное: слушатель oauth2Client.on('tokens', …) пишет обновлённые токены прямо обратно в таблицу. Backend передаёт полномочие один раз, а затем остаётся в стороне.
Часть про «возобновляемость», честно
Вот деталь, насчёт которой мне больше всего хочется быть прямым, потому что это ровно та вещь, которую changelog округляет. Примечание к релизу говорит «возобновляемые загрузки», и это правда с точки зрения пользователя. Но GeekBye не реализовал протокол возобновляемости. Нигде в исходниках GeekBye нет ни константы размера фрагмента, ни трекера байтового смещения, ни обработки Content-Range / 308 Resume Incomplete. Поищи их грепом — и не получишь ничего: они живут внутри 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 — это поток (createReadStream), а не буфер, — и передача потока это в точности то, что заставляет googleapis договориться о возобновляемой сессии за тебя, вместо того чтобы делать всё за один раз. Во-вторых, прогресс считывается из колбэка onUploadProgress SDK относительно fileStats.size. Это вся реализация «возобновляемой загрузки» на уровне приложения: поток внутрь, прогресс наружу. Трудный протокол — открыть сессию, загружать фрагментами, возобновить со смещения после обрыва — это работа SDK, и верным инженерным решением было дать ему выполнить эту работу, а не изобретать её заново.
Что переосмысляет весь релиз. Улучшение надёжности пришло не из написания движка загрузки. Оно пришло из удаления proxy, что стоял перед одним таким, — а раньше, на той же неделе, из удаления шага ffmpeg, стоявшего перед тем. Меньше кода, больше устойчивости, потому что устойчивой вещью была видавшая виды библиотека в тот миг, когда ты перестал вручную кормить её буфером.
Куски, что по-настоящему принадлежат GeekBye
Передача трансфера SDK не значит, что строить было нечего. Два куска настоящей логики приложения его окружают.
Откат осиротевшей папки. Каждая запись получает свою подпапку Drive (созданную с mimeType: 'application/vnd.google-apps.folder'), и транскрипт ложится туда вторым файлом рядом с видео. Если загрузка бросает исключение, catch удаляет эту папку — this.drive.files.delete({ fileId: folderId }), логируется как Rolled back orphaned Drive folder — так что неудавшаяся загрузка не оставляет следа пустых папок в твоём Drive. Создание контейнера прежде, чем ты знаешь, что операция удастся, — маленькая уязвимость; сделать так, чтобы путь ошибки его подчищал, — это исправление.
Жизненный цикл вокруг синглтона. DriveService — это синглтон, держащий в памяти клиент OAuth, и этот клиент в памяти не переживает перезапуск приложения, хотя токены переживают (они в базе данных). Так что CloudUploader заново инициализирует DriveService из сохранённых токенов перед каждой загрузкой, а значит запись, сделанная сразу после перезапуска, всё ещё выгружается, не прося тебя переподключиться. А ревизия превратила обработчик drive-connect в fire-and-forget, потому что блокирующий вызов connect тормозил опрос статуса рендерером — UI не мог показать «подключение…», если вызов connect держал поток. Эти два — реинициализация из хранилища и неблокирующий connect — настоящие граничные случаи прямого пути, и, что примечательно, ни один из них не про фрагментацию загрузки. Когда делегируешь трудную часть, баги, что тебе остаются, — это баги жизненного цикла.
Три вещи, которым нас научил этот релиз
- Спрашивай, куда текут байты, а не только работает ли оно. Proxy из v1.8.11 работал. Но прогонять каждую запись через собственные серверы стоит тебе трафика, памяти и возобновляемости — за файл, который ты не хранишь. Перечертить путь данных так, чтобы клиент говорил с назначением напрямую, — часто и есть вся оптимизация.
- Лучшая возобновляемая загрузка — та, что ты не писал. Передача потока зрелому SDK купила полную возобновляемую сессию за две строки кода. Инстинкт вручную склепать фрагментацию и арифметику смещений дал бы больше кода и меньше надёжности.
- Когда делегируешь ядро, наследуешь жизненный цикл, а не алгоритмы. Настоящими багами прямого клиента были «переинициализируй синглтон после перезапуска» и «не блокируй вызов connect», а не байтовые смещения. Это хорошая сделка — баги жизненного цикла видимы и локальны; баги протокола ни то, ни другое.
Про предыдущую главу истории v1 — рождён и исправлен за двенадцать минут (v1.8.10); а про всю дугу — анатомию доведения софта до совершенства.