
Tirando o backend do caminho de upload
A GeekBye grava sua tela e salva o vídeo no seu Google Drive. A primeira versão enviava cada gravação pelos próprios servidores da GeekBye no caminho; uma release depois, o arquivo ia direto da sua máquina para o Drive, e o backend foi rebaixado a segurar um único ponteiro. A parte interessante é o quão pouco código a versão «direta e retomável» realmente contém — porque a retomabilidade veio de apagar um proxy, não de escrever um.
A GeekBye pode gravar sua tela — vídeo, áudio do sistema e microfone — e soltar a gravação finalizada no seu Google Drive automaticamente. Entre a v1.8.11 e a v1.8.13, a descrição voltada ao usuário do recurso mal mudou, mas o caminho que o vídeo percorre para chegar ao Drive foi redirecionado por completo. Na primeira versão, sua gravação viajava pelos servidores da GeekBye. Na segunda, ia direto da sua máquina para o seu Drive, e o backend da GeekBye nunca viu um byte dela. Esse redirecionamento é a história toda, e a parte satisfatória é que a versão «melhor» contém visivelmente menos código do que a que substituiu.
O pipeline, de ponta a ponta
A gravação acontece no renderizador. ScreenRecordingService.ts cria um MediaRecorder sobre o stream de captura com { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } e o inicia com um timeslice de um segundo (CHUNK_INTERVAL_MS = 1000). Cada blob de ondataavailable é enviado via IPC para o processo principal, onde o handler recording:save-chunk o anexa a um arquivo no disco com fs.promises.appendFile. Vale sinalizar cedo, porque é algo tentador de interpretar errado: essa cadência de um segundo é o ritmo de escrita em disco, não um tamanho de pedaço de upload. Quando o upload começa, há um único arquivo WebM finalizado descansando no disco.
O que difere entre as duas releases é apenas o que acontece com esse arquivo finalizado.
O antes: tudo através do backend (v1.8.11)
O caminho de upload da v1.8.11, em CloudUploader.processRecording(), é o design óbvio e uma primeira versão completamente razoável:
POST /api/geekbye/recordingspara criar um registro no backend com os metadados da gravação.- Ler o vídeo inteiro na memória e enviá-lo por POST ao backend como multipart —
const fileBuffer = fs.readFileSync(filePath)— chegando aPOST /api/geekbye/recordings/${id}/upload. O backend então faz upload desse arquivo para o Drive e devolve umdriveUrl. POST …/processpara enviar a transcrição para a análise de IA.
O detalhe que sustenta tudo é o passo 2: o cliente carrega a gravação inteira na memória e cada byte dela transita pelos próprios servidores da GeekBye a caminho do Drive. Para um clipe curto, tudo bem. Para uma gravação de tela longa são três problemas de uma vez — pressão de memória no cliente por causa do readFileSync, custo de banda e de retransmissão no backend por um arquivo que ele nem guarda, e um único POST monolítico que tem que reiniciar do zero se a rede soluçar no meio do caminho.
Também vale ser honesto: esse «antes» não era um design original impecável. O recurso da v1.8.11 espremeu junto um pivô dentro da mesma semana: uma versão inicial fazia upload para o Cloudflare R2 via URLs pré-assinadas e convertia WebM para MP4 com um ffmpeg-static empacotado; isso foi substituído pelo fluxo do Drive mediado pelo backend; e então a conversão de ffmpeg foi arrancada por completo em favor de fazer upload do WebM como está (o commit anota que baixou o bitrate e produziu arquivos aproximadamente quatro vezes menores — uma estimativa de improviso, não um benchmark). Então até o «antes» já havia aprendido a apagar uma dependência nativa uma vez. A próxima release apagaria o proxy também.
O depois: direto para o Drive (v1.8.13)
A v1.8.13 reescreve CloudUploader.processRecording() em torno de uma única frase do commit que a publicou: fazer upload diretamente para o Drive, "keeping backend for metadata record + AI transcript analysis only." O novo caminho:
POST /api/geekbye/recordings— apenas metadados (título, duração, tamanho do arquivo,format: 'webm'). O comentário no código é direto:// Create backend record (metadata only — no file upload).- Fazer upload do vídeo diretamente para o Google Drive, pulando o intermediário do backend por completo.
PATCH /api/geekbye/recordings/${backendRecordingId}com o{ driveUrl, driveFolderId }resultante — o registro do backend é informado de onde o arquivo foi parar.- A transcrição ainda vai para
…/processpara análise.
O backend passou de estar no caminho do arquivo para ser informado sobre o arquivo depois do fato. Ele segura uma linha e um ponteiro; os bytes vivem só no disco do usuário e no Drive do usuário.
Virando um cliente de Drive de verdade
Para o cliente falar com o Drive diretamente, ele tem que ser um cliente de fato da Google Drive API, e a v1.8.13 adiciona a maquinaria para isso: um novo DriveService (o cliente do Drive), um novo DriveAuthRepository apoiado por uma nova tabela SQLite drive_auth, e o SDK googleapis como dependência.
A autorização é um repasse de capacidade limpo. O endpoint de app-config do backend retransmite as credenciais de cliente do Google OAuth — um clientId e um clientSecret — até o app, que CloudUploader lê e passa para driveService.initialize(...). A partir daí o app de desktop executa todo o fluxo OAuth ele mesmo: sobe um servidor http temporário numa porta 127.0.0.1 aleatória, abre a tela de consentimento no navegador do sistema com shell.openExternal, captura o redirecionamento num /callback, e troca o código por tokens com google-auth-library. Esses tokens são salvos localmente — saveDriveAuth(access_token, refresh_token, expiry_date) em drive_auth — com o escopo drive.file, que deixa o app gerenciar apenas os arquivos que cria. A renovação também é local: um listener oauth2Client.on('tokens', …) escreve os tokens renovados direto de volta na tabela. O backend entrega a capacidade uma vez e depois fica fora do laço.
A parte «retomável», honestamente
Aqui está o detalhe sobre o qual mais quero ser franco, porque é exatamente o tipo de coisa que um changelog arredonda. A nota de versão diz «uploads retomáveis», e isso é verdade do ponto de vista do usuário. Mas a GeekBye não implementou o protocolo resumable. Não há nenhuma constante de tamanho de pedaço, nenhum rastreador de offset de byte, nem tratamento de Content-Range / 308 Resume Incomplete em lugar nenhum do código-fonte da GeekBye. Procure por eles com grep e você não obtém nada — eles vivem dentro do SDK googleapis.
O que o próprio código da GeekBye faz é isto:
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)
},
},
)
Duas escolhas fazem todo o trabalho. Primeiro, media.body é um stream (createReadStream), não um buffer — e passar um stream é precisamente o que faz googleapis negociar uma sessão retomável em seu nome em vez de fazer de uma vez só. Segundo, o progresso é lido do callback onUploadProgress do SDK contra fileStats.size. Essa é toda a implementação de «upload retomável» no nível da aplicação: o stream entra, o progresso sai. O protocolo difícil — abrir uma sessão, fazer upload em pedaços, retomar de um offset após uma queda — é trabalho do SDK, e a decisão de engenharia correta foi deixá-lo fazer esse trabalho em vez de reinventá-lo.
O que reenquadra a release inteira. A melhoria de confiabilidade não veio de escrever um motor de upload. Veio de apagar o proxy que ficava na frente de um — e, antes na mesma semana, de apagar o passo de ffmpeg que ficava na frente daquele. Menos código, mais resiliência, porque a coisa resiliente era uma biblioteca mais que testada no momento em que você parou de alimentá-la à mão com um buffer.
As partes que são genuinamente da GeekBye
Entregar a transferência ao SDK não significa que não sobrou nada para construir. Duas peças de lógica de aplicação real a cercam.
Um rollback de pasta órfã. Cada gravação recebe sua própria subpasta no Drive (criada com mimeType: 'application/vnd.google-apps.folder'), e a transcrição aterrissa ali como um segundo arquivo ao lado do vídeo. Se o upload lança um erro, o catch apaga essa pasta — this.drive.files.delete({ fileId: folderId }), registrado como Rolled back orphaned Drive folder — para que um upload que falhe não deixe um rastro de pastas vazias no seu Drive. Criar um contêiner antes de saber que a operação vai ter sucesso é um pequeno passivo; fazer o caminho de falha limpá-lo é a correção.
Ciclo de vida em torno de um singleton. DriveService é um singleton que segura um cliente OAuth em memória, e esse cliente em memória não sobrevive a um reinício do app mesmo que os tokens sobrevivam (eles estão no banco de dados). Então CloudUploader reinicializa DriveService a partir dos tokens guardados antes de cada upload, o que significa que uma gravação feita logo após relançar ainda faz upload sem pedir que você se reconecte. E a passada de revisão transformou o handler drive-connect em fire-and-forget, porque uma chamada de conexão bloqueante estava travando o polling de status do renderizador — a UI não conseguia mostrar «conectando…» se a chamada de conexão segurasse a thread. Essas duas — reinicializar-do-armazenamento e conexão não bloqueante — são os verdadeiros casos extremos do caminho direto, e notavelmente nenhuma das duas é sobre o fatiamento do upload. Quando você delega a parte difícil, os bugs que sobram para você são bugs de ciclo de vida.
Três coisas que esta release nos ensinou
- Pergunte por onde os bytes fluem, não só se funciona. O proxy da v1.8.11 funcionava. Mas rotear cada gravação pelos seus próprios servidores custa banda, memória e capacidade de reinício por um arquivo que você não guarda. Redesenhar o caminho de dados para que o cliente fale direto com o destino é muitas vezes a otimização inteira.
- O melhor upload retomável é o que você não escreveu. Passar um stream para um SDK maduro comprou uma sessão retomável completa por duas linhas de código. O instinto de programar à mão o fatiamento e a aritmética de offset teria produzido mais código e menos confiabilidade.
- Quando você delega o núcleo, você herda ciclo de vida, não algoritmos. Os verdadeiros bugs do cliente direto eram «reinicializar o singleton após o reinício» e «não bloquear a chamada de conexão», não offsets de bytes. É uma boa troca — bugs de ciclo de vida são visíveis e locais; bugs de protocolo não são nem uma coisa nem outra.
Para o capítulo anterior da história v1, nascido e corrigido em doze minutos (v1.8.10); e para todo o arco, a anatomia de entregar software até a perfeição.