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?
Lê 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
- 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';
- 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]);
- O JS da tela (incluindo Foto Self) manda esse path para o endpoint de stream. Não abre
uploads/nemgdrive:como URL estática:
function documentStreamUrl(establishmentId, path, extraParams) {
// ...
params.set('estabelecimento_id', String(establishmentId || ''));
params.set('path', path);
return base + '?' + params.toString();
}
backend/api/estabelecimentos/documento.phpvalida 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)
| Coluna | Valor exacto | Exemplo |
|---|---|---|
arquivo_path / caminho | gdrive:{fileId}/{nomeOriginal} | gdrive:1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ/contrato.pdf |
nome_original | nome do ficheiro enviado | contrato.pdf |
drive_file_id | ID puro, sem prefixo | 1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ |
drive_view_url | URL pública do Drive (webViewLink ou fallback) | https://drive.google.com/file/d/1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ/view |
drive_mime_type | MIME detectado | application/pdf |
drive_size_bytes | tamanho em bytes | 184320 |
Não é drive:{fileId}. Uploads novos gravam gdrive:.
Trecho que grava no banco
backend_establishment_save_typed_documents (establishment_persist_service.php):
- 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 (?, ?, ?, ?)";
- UPDATE separado dos metadados Drive, só se
drive_file_idvier 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–169compliance_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
- Google Drive configurado no servidor (
GOOGLE_DRIVE_USER_MEDIA_FOLDER_ID+ credenciais). Sem isso,backend_storage_metadevolveexists: falsee a API responde 404: «Arquivo do documento não encontrado no servidor.» - A service account tem acesso ao ficheiro no Shared Drive.
- 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.pdfdepois do prefixo. - Re-upload do mesmo tipo usa
backend_drive_existing_file_id, que também parseia o path — o replace funciona mesmo comdrive:e semdrive_file_id. - Logs de upload novo marcam
is_drive_refsó comstr_starts_with(..., 'gdrive:')(upload-documento.phplinha 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
| Coluna | Formato esperado | Exemplo |
|---|---|---|
drive_file_id | ID puro do Drive, sem drive: / gdrive: | 1ORmmzQjp-lbEYW5VipY3eGJH-THJb0mQ |
drive_view_url | URL do Google Drive, não endpoint interno | https://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
| Tabela | Coluna(s) de path | Colunas Drive próprias? | Upload hoje grava | Leitura / stream |
|---|---|---|---|---|
estabelecimentos_documentos | arquivo_path ou caminho | Sim: drive_file_id, drive_view_url, drive_mime_type, drive_size_bytes | gdrive:{id}/{nome} + metadados | Path → documento.php |
estabelecimentos_locais | contrato_assinado, contrato_estatuto, cartao_cnpj, rg_cnh_representante, comprovante_endereco, foto_fachada | Não | Mesmo path do documento (espelho legado) | Fallback do catálogo se o tipado estiver vazio |
compliance_estabelecimentos | As mesmas 6 colunas de path | Não | Mesmo path no upload (se existir dossiê) | Fallback do catálogo + allow-list do stream |
task_documents | file_path | Não | gdrive:{id}/{nome} em file_path | backend/api/tarefas/documento.php lê file_path |
white_labels | logotipo | Sim: logo_drive_file_id, logo_view_url, logo_mime_type, logo_size_bytes | logotipo = gdrive:{id}/{nome}; meta em logo_* | PHP parseia logotipo; URL pública https://lh3.googleusercontent.com/d/{id} |
users | cnh, rg_doc, cpf_doc, contrato_social se as colunas existirem (schema baseline não as tem) | Não | Path gdrive:{id}/{nome} | Fallback se clientes estiver vazio |
clientes | cnh, rg_doc, cpf_doc, contrato_social (+ outras: comprovante_residencia, arquivo_adicionalN, …) | Não | Path gdrive:{id}/{nome} | Fonte primária de backend/api/usuarios/documento.php |
lead_proposals | attachment_path | Não | Ninguém grava anexo hoje (INSERT não inclui o campo) | Sem endpoint de stream próprio |
lead_photos (extra) | photo_reference | Não | gdrive:{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):
| Coluna | Valor |
|---|---|
logotipo | gdrive:{fileId}/{nome} (ex. gdrive:xxx/wl-logo.png) |
logo_drive_file_id | ID puro |
logo_view_url | https://drive.google.com/file/d/{id}/view (ou webViewLink) |
logo_mime_type | image/png etc. |
logo_size_bytes | bytes |
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):
- Lê
clientes.{campo}pelouser_id - Se vazio e
userstiver as colunas, lêusers.{campo} 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_adicional1–4, …) 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):
- Obrigatório para a tela:
arquivo_path(oucaminho) =drive:{fileId}ou (melhor)gdrive:{fileId}/{nomeOriginal}. - Opcional:
drive_file_id= ID puro;drive_view_url=https://drive.google.com/file/d/{id}/view. - Se o EC tiver cópia nas colunas de
estabelecimentos_locais/compliance_estabelecimentos, actualiza o mesmo path nessas colunas. Não hádrive_*lá. - 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 banco | Serve? | Nome no download |
|---|---|---|
gdrive:1ORmm…0mQ/contrato.pdf | Sim (canónico) | contrato.pdf |
gdrive:1ORmm…0mQ | Sim | o próprio ID |
drive:1ORmm…0mQ | Sim (o que já migraste) | o próprio ID |
drive:1ORmm…0mQ/contrato.pdf | Sim | contrato.pdf |
https://drive.google.com/file/d/1ORmm…0mQ/view | Sim (parser de URL) | o ID |
uploads/estabelecimentos/ficheiro.pdf | Só se o ficheiro existir em disco | ficheiro.pdf |
7. Ficheiros-chave
| Papel | Ficheiro |
|---|---|
Parse gdrive: / drive: + upload/download | backend/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 EC | backend/api/estabelecimentos/upload-documento.php |
| Stream / preview do EC | backend/api/estabelecimentos/documento.php |
| Catálogo da tela | backend/src/services/compliance/compliance_service.php |
| JS da tela do EC | assets/js/pages/estabelecimentos/estabelecimentos-detalhe-satellites.js |
| Migration das colunas Drive | database/migrations/20260903_google_drive_metadata.php |