Pular para o conteúdo principal

Google Drive — como o app lê e grava arquivos

Documento de investigação do mecanismo real de storage (disco vs Google Drive). Serve para migrar registros legados sem quebrar a tela.

Conclusão curta: a tela e o download não leem drive_file_id nem drive_view_url. Eles leem a coluna de caminho (arquivo_path / caminho / file_path / logotipo / etc.), interpretam o prefixo gdrive: ou drive:, e baixam o ficheiro pelo ID extraído. Um registro migrado só com arquivo_path = drive:{fileId} funciona na tela atual, desde que o Drive esteja configurado no servidor.


1. Qual coluna o app lê ao exibir / baixar um documento de EC?

arquivo_path (ou caminho, se essa for a coluna existente).
Não lê drive_file_id.
Não lê drive_view_url.
Não existe coluna view_url — o nome real é drive_view_url.

Fluxo na tela

  1. A listagem hidrata o catálogo com o path tipado:
$pathCol = backend_establishment_document_path_column($conn);
$stmt = $conn->prepare("SELECT tipo, {$pathCol} AS caminho FROM estabelecimentos_documentos WHERE estabelecimento_id = ?");
// ...
$caminho = (string) ($row['caminho'] ?? '');
// ...
$out[$tipo] = $caminho;

A coluna física é resolvida em runtime: primeiro arquivo_path, senão caminho.

* Coluna de path em estabelecimentos_documentos (arquivo_path no schema atual; caminho legado).
function backend_establishment_document_path_column(mysqli $conn): string
{
// ...
foreach (['arquivo_path', 'caminho'] as $candidate) {
// ...
if ($ok) {
$cache[$key] = $candidate;
return $cache[$key];
}
}
$cache[$key] = 'arquivo_path';
  1. O catálogo devolve esse path em item.file (sem olhar metadados Drive):
if (backend_establishment_document_path_filled($typed[$key] ?? null)) {
$path = trim((string) $typed[$key]);
  1. O JS da tela (incluindo Foto Self) manda esse path para o endpoint de stream. Não abre uploads/ nem gdrive: como URL estática:
function documentStreamUrl(establishmentId, path, extraParams) {
// ...
params.set('estabelecimento_id', String(establishmentId || ''));
params.set('path', path);
return base + '?' + params.toString();
}
  1. backend/api/estabelecimentos/documento.php valida que o path pertence ao EC e serve o ficheiro:
$meta = backend_storage_meta($relative);
// ...
if (!backend_storage_stream($relative, $download, $filename, $meta['mime'] !== '' ? $meta['mime'] : null)) {

Função que decide disco vs Drive

backend_storage_download / backend_storage_stream / backend_storage_meta em backend/src/services/storage/google_drive_storage.php.

A decisão não consulta o banco de metadados. Ela parseia a string do path:

function backend_drive_parse_ref(string $path): ?array
{
// aceita:
// gdrive:{fileId}
// gdrive:{fileId}/{nomeOriginal}
// drive:{fileId}
// drive:{fileId}/{nomeOriginal}
// https://drive.google.com/file/d/{fileId}/...
// ?id={fileId}
// https://lh3.googleusercontent.com/d/{fileId}
}

Prefixo canónico de escrita nova: gdrive: (BACKEND_DRIVE_REF_PREFIX em backend/src/services/storage/google_drive_media.php, linha 14).
Prefixo drive: é alias de leitura (compatibilidade).

Se backend_drive_parse_ref($path) devolver um fileId:

$parsed = backend_drive_parse_ref($relativePath);
if ($parsed !== null) {
// baixa via GoogleDriveService::downloadFile($parsed['fileId'])
}

Se não for ref Drive, procura ficheiro local em uploads/... (backend_storage_absolute_path).

O download no Drive usa o ID puro (sem prefixo):

public function downloadFile(string $fileId): array
{
$meta = $this->getFileMeta($fileId);
$buffer = $this->downloadMedia($meta['id']);
// GET https://www.googleapis.com/drive/v3/files/{fileId}?alt=media&supportsAllDrives=true
}

2. Upload novo hoje — o que é gravado?

Com Drive configurado (backend_drive_should_store_remote() = true), o upload devolve isto:

$result = [
'path' => backend_drive_make_ref($uploaded['fileId'], $original !== '' ? $original : $fileName),
'nome_original' => $original !== '' ? $original : $fileName,
'drive_file_id' => $uploaded['fileId'],
'drive_view_url' => $uploaded['viewUrl'],
'drive_mime_type' => $mime,
'drive_size_bytes' => $size > 0 ? $size : strlen($bytes),
];

backend_drive_make_ref monta o path canónico:

return $name !== ''
? BACKEND_DRIVE_REF_PREFIX . $id . '/' . $name
: BACKEND_DRIVE_REF_PREFIX . $id;

viewUrl vem do webViewLink da API Google; se vazio, fallback:

$viewUrl = (string) $created->getWebViewLink();
if ($viewUrl === '') {
$viewUrl = 'https://drive.google.com/file/d/' . $fileId . '/view';
}
return ['fileId' => $fileId, 'viewUrl' => $viewUrl];

Formato exacto de cada coluna (upload novo, Drive ligado)

ColunaValor exactoExemplo
arquivo_path / caminhogdrive:{fileId}/{nomeOriginal}gdrive:1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ/contrato.pdf
nome_originalnome do ficheiro enviadocontrato.pdf
drive_file_idID puro, sem prefixo1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ
drive_view_urlURL pública do Drive (webViewLink ou fallback)https://drive.google.com/file/d/1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ/view
drive_mime_typeMIME detectadoapplication/pdf
drive_size_bytestamanho em bytes184320

Não é drive:{fileId}. Uploads novos gravam gdrive:.

Trecho que grava no banco

backend_establishment_save_typed_documents (establishment_persist_service.php):

  1. INSERT/UPSERT do path + nome_original:
? "INSERT INTO estabelecimentos_documentos (estabelecimento_id, tipo, {$pathCol}, nome_original, status) VALUES (?, ?, ?, ?, 'PENDENTE')"
: "INSERT INTO estabelecimentos_documentos (estabelecimento_id, tipo, {$pathCol}, nome_original) VALUES (?, ?, ?, ?)";
  1. UPDATE separado dos metadados Drive, só se drive_file_id vier preenchido:
if ($hasDriveCols && $driveFileId) {
$upd = $conn->prepare('
UPDATE estabelecimentos_documentos
SET drive_file_id = ?, drive_view_url = ?, drive_mime_type = ?, drive_size_bytes = ?
WHERE estabelecimento_id = ? AND tipo = ?
');

Se o persist receber só uma string drive:{fileId} (sem array), ele deriva drive_file_id do parse e deixa drive_view_url vazio:

$parsed = backend_drive_parse_ref($path);
if ($parsed) {
if ($driveFileId === null || $driveFileId === '') {
$driveFileId = $parsed['fileId'];
}

Espelhos no mesmo upload

backend/api/estabelecimentos/upload-documento.php também copia o mesmo path para colunas legadas, se existirem:

  • estabelecimentos_locais.{coluna} (ex.: foto_fachada, contrato_assinado) — linhas 150–169
  • compliance_estabelecimentos.{coluna} — linhas 196–208

Essas tabelas não têm drive_file_id / drive_view_url.

Fallback local (Drive desligado)

return [
'path' => 'uploads/estabelecimentos/' . $nomeUnico,
'nome_original' => $original !== '' ? $original : $nomeUnico,
'drive_file_id' => null,
'drive_view_url' => null,
'drive_mime_type' => $mime,
'drive_size_bytes' => $size,
];

3. Registro legado só com arquivo_path = drive:{fileId} e metadados NULL — funciona?

Sim, funciona na tela atual e no download.

Exemplo que o parser aceita:

arquivo_path = drive:1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ
drive_file_id = NULL
drive_view_url = NULL

backend_drive_parse_ref('drive:1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ') devolve { fileId: '1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ', name: '' }.
O stream baixa esse ID. drive_file_id e drive_view_url não entram no fluxo.

Condições para não quebrar

  1. Google Drive configurado no servidor (GOOGLE_DRIVE_USER_MEDIA_FOLDER_ID + credenciais). Sem isso, backend_storage_meta devolve exists: false e a API responde 404: «Arquivo do documento não encontrado no servidor.»
  2. A service account tem acesso ao ficheiro no Shared Drive.
  3. O path passado no query string é exactamente o valor da coluna (o endpoint compara o path com o catálogo).

Efeitos colaterais (não quebram, mas são piores que um upload nativo)

  • Nome do ficheiro no download fica o próprio ID (1ORmmzQjp-...), porque não há /nome.pdf depois do prefixo.
  • Re-upload do mesmo tipo usa backend_drive_existing_file_id, que também parseia o path — o replace funciona mesmo com drive: e sem drive_file_id.
  • Logs de upload novo marcam is_drive_ref só com str_starts_with(..., 'gdrive:') (upload-documento.php linha 242). Isso é só log, não a leitura.

Não é obrigatório preencher drive_file_id / drive_view_url para a tela funcionar. São metadados auxiliares (auditoria, tamanho, URL humana). Se quiseres alinhar com uploads novos, preenche — ver secção 4.


4. Formato se fores preencher drive_file_id e drive_view_url

ColunaFormato esperadoExemplo
drive_file_idID puro do Drive, sem drive: / gdrive:1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ
drive_view_urlURL do Google Drive, não endpoint internohttps://drive.google.com/file/d/1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ/view

Não uses backend/api/estabelecimentos/documento.php?... em drive_view_url.
Não uses https://lh3.googleusercontent.com/d/{id} aí — esse formato é só para logotipo público (backend_drive_public_image_url).

Recomendação de path para o resto da migração (igual ao upload nativo):

gdrive:{fileId}/{nomeOriginal}

drive:{fileId} continua válido na leitura. gdrive:{fileId}/{nome} é o que o código grava hoje e preserva o nome do ficheiro.


5. Outras tabelas que vais migrar

Padrão geral: quase todas seguem o path-string, não colunas Drive próprias. Só estabelecimentos_documentos e white_labels têm metadados Drive.

Matriz

TabelaColuna(s) de pathColunas Drive próprias?Upload hoje gravaLeitura / stream
estabelecimentos_documentosarquivo_path ou caminhoSim: drive_file_id, drive_view_url, drive_mime_type, drive_size_bytesgdrive:{id}/{nome} + metadadosPath → documento.php
estabelecimentos_locaiscontrato_assinado, contrato_estatuto, cartao_cnpj, rg_cnh_representante, comprovante_endereco, foto_fachadaNãoMesmo path do documento (espelho legado)Fallback do catálogo se o tipado estiver vazio
compliance_estabelecimentosAs mesmas 6 colunas de pathNãoMesmo path no upload (se existir dossiê)Fallback do catálogo + allow-list do stream
task_documentsfile_pathNãogdrive:{id}/{nome} em file_pathbackend/api/tarefas/documento.phpfile_path
white_labelslogotipoSim: logo_drive_file_id, logo_view_url, logo_mime_type, logo_size_byteslogotipo = gdrive:{id}/{nome}; meta em logo_*PHP parseia logotipo; URL pública https://lh3.googleusercontent.com/d/{id}
userscnh, rg_doc, cpf_doc, contrato_social se as colunas existirem (schema baseline não as tem)NãoPath gdrive:{id}/{nome}Fallback se clientes estiver vazio
clientescnh, rg_doc, cpf_doc, contrato_social (+ outras: comprovante_residencia, arquivo_adicionalN, …)NãoPath gdrive:{id}/{nome}Fonte primária de backend/api/usuarios/documento.php
lead_proposalsattachment_pathNãoNinguém grava anexo hoje (INSERT não inclui o campo)Sem endpoint de stream próprio
lead_photos (extra)photo_referenceNãogdrive:{id}/{nome}backend/api/comercial/lead-photo.php

A migration database/migrations/20260903_google_drive_metadata.php só adiciona colunas Drive em:

  • estabelecimentos_documentos (drive_*)
  • white_labels (logo_drive_file_id, logo_view_url, logo_mime_type, logo_size_bytes, drive_quota_mb)

5.1 estabelecimentos_locais

Não tem lógica Drive própria. As 6 colunas são cópia do path. VARCHAR(255) no baseline — um gdrive:{id}/{nome} cabe; um path muito longo pode truncar.

Leitura: o catálogo só usa essas colunas se estabelecimentos_documentos não tiver o tipo preenchido (compliance_service.php linhas 158–164).

Para migrar: actualiza as mesmas colunas com o mesmo valor que puseres em arquivo_path (gdrive: ou drive:). Sem colunas drive_* para preencher.

5.2 compliance_estabelecimentos

Igual ao item anterior: path nas 6 colunas, VARCHAR(500). Sem drive_file_id.

O stream de compliance (backend/api/compliance/documento.php) também só usa o path:

if (!backend_storage_stream(
$resolved['relative'],
$download,
$resolved['filename'],
$resolved['mime'] !== '' ? $resolved['mime'] : null
)) {

drive:{fileId} na coluna de path funciona. Metadados Drive não existem nesta tabela.

5.3 task_documents

Schema (tasks_service.php 110–123): file_path VARCHAR(500), sem colunas Drive.

Upload (tasks_service.php 1446–1469):

$stored = backend_storage_store_module_file(...);
$relative = $stored['path']; // gdrive:{id}/{nome}
INSERT INTO task_documents (..., file_path, ...) VALUES (..., $relative, ...);

drive_file_id / drive_view_url do $stored são descartados.

Stream: backend_tasks_document_stream_path lê só d.file_path (linhas 1819–1845) → backend_storage_stream.

Migrar: file_path = drive:{fileId} ou gdrive:{fileId}/{nome}. Nada mais.

5.4 white_labels

Tem colunas Drive próprias, mas a leitura do logo usa logotipo:

if ($whiteLabelId > 0) {
$stmt = $conn->prepare('SELECT logotipo FROM white_labels WHERE id = ? LIMIT 1');
// ...
$logoPath = (string) ($row['logotipo'] ?? '');

backend_white_label_logo_url_for_record parseia logotipo. Se for ref Drive, devolve URL pública (não o proxy interno):

$parsed = backend_drive_parse_ref($logo);
if ($parsed !== null) {
return backend_drive_public_image_url($parsed['fileId']);
// https://lh3.googleusercontent.com/d/{fileId}
}

Upload novo (backend_storage_store_white_label_logo + backend_white_label_persist_logo_meta):

ColunaValor
logotipogdrive:{fileId}/{nome} (ex. gdrive:xxx/wl-logo.png)
logo_drive_file_idID puro
logo_view_urlhttps://drive.google.com/file/d/{id}/view (ou webViewLink)
logo_mime_typeimage/png etc.
logo_size_bytesbytes

Logos no Drive são públicos (uploadPublicFile + permissão anyone/reader). Documentos de EC são privados.

Atenção no JS: assets/js/white-label-theme.js linhas 67–71 só reconhece o prefixo gdrive:, não drive:. Se migrares logo com drive:{id}, o PHP ainda resolve; o fallback JS do tema não. Prefere gdrive:{id}/{nome} em logotipo.

drive:{fileId} em logotipo com logo_* NULL funciona no PHP. logo_* são opcionais para exibir.

5.5 users e clientes

Mesmo padrão de path, sem colunas Drive.

Upload (user_document_service.php):

$caminhos[$column] = $stored['path'];

$stored['drive_file_id'] só vai para log. Persist grava o path em clientes (sempre) e em users (só se existirem as colunas cnh etc.).

Stream (backend_users_document_stream_path):

  1. clientes.{campo} pelo user_id
  2. Se vazio e users tiver as colunas, lê users.{campo}
  3. backend_storage_stream($relative) — de novo, parse do path

Campos usados pelo app: cnh, rg_doc, cpf_doc, contrato_social.
clientes ainda tem outros paths (comprovante_residencia, arquivo_adicional14, …) que o stream de utilizador não expõe.

Migrar: cnh / rg_doc / cpf_doc / contrato_social = drive:{fileId} ou gdrive:{fileId}/{nome}. Sem drive_file_id.

5.6 lead_proposals

Tem attachment_path VARCHAR(255), mas o INSERT de proposta (leads_service.php 1541–1546) não grava ficheiro. Não há UPDATE attachment_path no backend.

Não há endpoint que sirva lead_proposals.attachment_path.

Se no futuro usares essa coluna, o valor correcto (pelo padrão do resto do sistema) é o path-string gdrive:{id}/{nome}. Sem colunas Drive.

Fotos de lead (tabela à parte lead_photos) já usam o padrão: photo_reference = gdrive:... e stream em lead-photo.php.

5.7 Credenciamento PayUp (efeito da migração)

payup_documents_service.php materializa o path (backend_storage_materialize) antes de enviar à API. drive:{fileId} e gdrive:{fileId} funcionam — o helper descarrega o Drive para um temp. Sem Drive configurado, o documento é skipped.


6. Como migrar o resto (checklist)

Para documentos de EC (estabelecimentos_documentos):

  1. Obrigatório para a tela: arquivo_path (ou caminho) = drive:{fileId} ou (melhor) gdrive:{fileId}/{nomeOriginal}.
  2. Opcional: drive_file_id = ID puro; drive_view_url = https://drive.google.com/file/d/{id}/view.
  3. Se o EC tiver cópia nas colunas de estabelecimentos_locais / compliance_estabelecimentos, actualiza o mesmo path nessas colunas. Não há drive_* lá.
  4. Não deixes arquivo_path = uploads/estabelecimentos/... se o ficheiro já não está no disco — a UI vai 404.

Para as outras tabelas: muda só a coluna de path. Não esperes (nem precisas) de drive_file_id / view_url.

Prefixos aceites na leitura:

Valor no bancoServe?Nome no download
gdrive:1ORmm…0mQ/contrato.pdfSim (canónico)contrato.pdf
gdrive:1ORmm…0mQSimo próprio ID
drive:1ORmm…0mQSim (o que já migraste)o próprio ID
drive:1ORmm…0mQ/contrato.pdfSimcontrato.pdf
https://drive.google.com/file/d/1ORmm…0mQ/viewSim (parser de URL)o ID
uploads/estabelecimentos/ficheiro.pdfSó se o ficheiro existir em discoficheiro.pdf

7. Ficheiros-chave

PapelFicheiro
Parse gdrive: / drive: + upload/downloadbackend/src/services/storage/google_drive_storage.php
Cliente Google Drive (upload, webViewLink)backend/src/services/storage/google_drive_service.php
Prefixo canónico gdrive:backend/src/services/storage/google_drive_media.php
Gravação EC (arquivo_path + drive_*)backend/src/services/estabelecimentos/establishment_persist_service.php
Upload HTTP do ECbackend/api/estabelecimentos/upload-documento.php
Stream / preview do ECbackend/api/estabelecimentos/documento.php
Catálogo da telabackend/src/services/compliance/compliance_service.php
JS da tela do ECassets/js/pages/estabelecimentos/estabelecimentos-detalhe-satellites.js
Migration das colunas Drivedatabase/migrations/20260903_google_drive_metadata.php