Entradas criadas com ZipInfo() nascem com external_attr=0; o unzip cria os diretórios como drw------- e o webserver não consegue atravessá-los — plugin some da lista ou dá 'Unable to load plugin information' (Plugin.php linha 882). Inclui forma correta, sintoma do Jan 1 1980, validação do ZIP e comando de correção no servidor. Diagnosticado no build VIP do butterfly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
192 lines
6.4 KiB
Markdown
192 lines
6.4 KiB
Markdown
---
|
|
id: KB-PLUGIN-018
|
|
title: "Convenção de wrapper directory na geração de ZIP de release de plugin (script mindplace-release.sh)"
|
|
domain: plugin-dev
|
|
tags:
|
|
- mindplace
|
|
- forgejo
|
|
- release
|
|
- zip
|
|
- wrapper
|
|
- mindplace-release-sh
|
|
status: active
|
|
severity: high
|
|
created_at: 2026-05-21
|
|
updated_at: 2026-05-21
|
|
applies_to:
|
|
- Qualquer plugin publicado via bin/mindplace-release.sh
|
|
- Plugins distribuídos pelo catálogo Mindplace
|
|
related:
|
|
- KB-PLUGIN-013
|
|
- KB-PLUGIN-017
|
|
---
|
|
|
|
# KB-PLUGIN-018 — Convenção de wrapper directory para ZIPs de release
|
|
|
|
## Contexto
|
|
|
|
ZIPs de release de plugin GLPI distribuídos via Mindplace devem ter um **wrapper directory consistente** no topo (uma pasta com o nome do plugin envolvendo todos os arquivos). Sem isso, o extrator do Mindplace podia mutilar paths em versões anteriores ao fix descrito em [KB-PLUGIN-017](KB-PLUGIN-017-mindplace-zip-wrapper-detector-bug.md).
|
|
|
|
Mesmo após o fix do detector, manter o wrapper continua sendo a forma "correta" e mais robusta: dá ao extrator um prefix bem-definido para remover, e remove ambiguidade na inspeção do ZIP.
|
|
|
|
## Estrutura esperada
|
|
|
|
```
|
|
mcprotocol-1.0.3.zip
|
|
└── mcprotocol/ ← wrapper directory (nome = key do plugin = basename do diretório)
|
|
├── setup.php
|
|
├── hook.php
|
|
├── ajax/
|
|
│ └── mcp.php
|
|
├── src/
|
|
│ ├── Boot.php
|
|
│ └── Server.php
|
|
└── public/
|
|
└── logo.png
|
|
```
|
|
|
|
Verificação rápida:
|
|
|
|
```bash
|
|
unzip -l <plugin>-<ver>.zip | head
|
|
# Primeiro entry DEVE ser "<plugin>/"
|
|
# Todos os demais entries DEVEM começar com "<plugin>/"
|
|
```
|
|
|
|
## Fluxo oficial (automatizado)
|
|
|
|
Use o script `bin/mindplace-release.sh` documentado em [KB-PLUGIN-013](KB-PLUGIN-013-mindplace-plugin-publication-runbook.md). Desde 2026-05-21, a Fase 3 do script gera ZIPs com wrapper directory automaticamente — rodando `zip` do **diretório pai** do plugin:
|
|
|
|
```bash
|
|
# Trecho relevante de bin/mindplace-release.sh (Fase 3)
|
|
PARENT_DIR="$(dirname "$PLUGIN_DIR")"
|
|
PLUGIN_BASENAME="$(basename "$PLUGIN_DIR")"
|
|
cd "$PARENT_DIR"
|
|
zip -r "$ZIP_PATH" "$PLUGIN_BASENAME" \
|
|
--exclude "$PLUGIN_BASENAME/.git*" \
|
|
--exclude "$PLUGIN_BASENAME/node_modules/*" \
|
|
--exclude "$PLUGIN_BASENAME/vendor/*" \
|
|
--exclude "*.DS_Store" \
|
|
--exclude "*.zip" \
|
|
-q
|
|
|
|
# Sanity check: aborta se wrapper não aparecer no topo
|
|
FIRST_ENTRY=$(unzip -Z1 "$ZIP_PATH" | head -1)
|
|
if [ "$FIRST_ENTRY" != "$PLUGIN_BASENAME/" ]; then
|
|
err "ZIP gerado sem wrapper directory esperado..."
|
|
fi
|
|
```
|
|
|
|
O sanity check no fim falha rápido se algum dia o ZIP voltar a sair sem wrapper.
|
|
|
|
## Geração manual (fallback)
|
|
|
|
Se precisar gerar localmente fora do script:
|
|
|
|
```bash
|
|
# A partir do diretório PAI da pasta do plugin
|
|
cd /home/glpi/glpi_dev/plugins/
|
|
zip -r mcprotocol-1.0.3.zip mcprotocol/ \
|
|
-x "mcprotocol/.git*" \
|
|
-x "*.zip" \
|
|
-x "mcprotocol/node_modules/*" \
|
|
-x "mcprotocol/vendor/*"
|
|
```
|
|
|
|
**Não** fazer `cd plugin && zip -r ...zip .` — esse padrão produz ZIP sem wrapper e era o bug histórico do script.
|
|
|
|
## Gotcha — ZIP gerado com `python zipfile` precisa de permissões explícitas
|
|
|
|
Quando o `zip` CLI não está disponível e o pacote é montado com `zipfile`
|
|
(cenário comum neste ambiente, ver [[redmine-plugin-project]]), as entradas
|
|
criadas com `zipfile.ZipInfo(nome)` nascem com `external_attr = 0` — sem bits
|
|
de permissão e sem a flag MS-DOS de diretório. O `unzip` então cria as pastas
|
|
como **`drw-------`** (sem bit de execução), e o resultado no servidor é:
|
|
|
|
- o usuário do webserver **não consegue atravessar** o diretório do plugin;
|
|
- `Plugin::isLoadable()` não enxerga o `setup.php`;
|
|
- o plugin **some da lista** sem nenhum erro, ou aparece
|
|
`Unable to load plugin "<key>" information. at Plugin.php line 882`
|
|
quando já existe registro dele no banco.
|
|
|
|
Diagnosticado em 2026-07-31 no build VIP do butterfly instalado no HML de um
|
|
cliente (`drw------- root root` na pasta do plugin).
|
|
|
|
### Forma correta
|
|
|
|
```python
|
|
DIR_ATTR = (0o40755 << 16) | 0x10 # drwxr-xr-x + flag de diretório
|
|
FILE_ATTR = (0o100644 << 16) # -rw-r--r--
|
|
|
|
zi = zipfile.ZipInfo(arcname, date_time=(2026, 7, 31, 12, 0, 0))
|
|
zi.external_attr = DIR_ATTR # entradas de diretório
|
|
z.writestr(zi, '')
|
|
|
|
zi = zipfile.ZipInfo.from_file(src, arcname)
|
|
zi.external_attr = FILE_ATTR # arquivos
|
|
zi.compress_type = zipfile.ZIP_DEFLATED
|
|
```
|
|
|
|
Sintoma acessório que denuncia o problema: data **`Jan 1 1980`** no diretório
|
|
extraído (timestamp default de `ZipInfo` sem `date_time`).
|
|
|
|
### Validação obrigatória do ZIP
|
|
|
|
```bash
|
|
# extrair em pasta temporária e conferir os modos
|
|
unzip -q <plugin>-<ver>.zip -d /tmp/ziptest && ls -ld /tmp/ziptest/<plugin>
|
|
# esperado: drwxr-xr-x (NUNCA drw-------)
|
|
```
|
|
|
|
Correção no servidor quando o ZIP defeituoso já foi extraído:
|
|
|
|
```bash
|
|
chown -R www-data:www-data <glpi>/plugins/<key>
|
|
find <glpi>/plugins/<key> -type d -exec chmod 755 {} \;
|
|
find <glpi>/plugins/<key> -type f -exec chmod 644 {} \;
|
|
```
|
|
|
|
## .gitignore padrão do plugin
|
|
|
|
Para que o ZIP não inclua arquivos sensíveis ou de teste, garantir `.gitignore` na raiz do plugin:
|
|
|
|
```gitignore
|
|
# Dependências
|
|
vendor/
|
|
node_modules/
|
|
|
|
# macOS
|
|
.DS_Store
|
|
**/.DS_Store
|
|
|
|
# IDE
|
|
.idea/
|
|
.vscode/
|
|
|
|
# Logs
|
|
*.log
|
|
|
|
# Build artifacts
|
|
*.zip
|
|
|
|
# Arquivos de teste (não publicar)
|
|
ajax/test*.php
|
|
ajax/mcp_test.php
|
|
front/test*.php
|
|
```
|
|
|
|
O script `mindplace-release.sh` excludes `.git*` por padrão; o `.gitignore` em si pode ser commitado e publicado sem problema.
|
|
|
|
## Erros comuns
|
|
|
|
| Sintoma | Causa | Fix |
|
|
|---|---|---|
|
|
| Após instalar via Mindplace, arquivos com nomes mutilados (`.php`, `p.php`, pastas `ic/`, `t/`) | ZIP gerado sem wrapper + Mindplace com detector buggy | Atualizar Mindplace ([KB-PLUGIN-017]) **e** regerar ZIP com wrapper |
|
|
| Após instalar, pasta `mcprotocol/mcprotocol/` aninhada | ZIP com wrapper mas extrator versão muito antiga, ou wrapper com nome diferente do `key` | Conferir que o basename do diretório bate com o `key` em plugins.json |
|
|
| Release não aparece no Mindplace | Release sem asset `.zip` no Forgejo | Verificar que a Fase 4 do script subiu o asset; tem rate limit ou erro? |
|
|
|
|
## Veja também
|
|
|
|
- [KB-PLUGIN-011](KB-PLUGIN-011-mindplace-release-zip-procedure.md) — Procedimento de release **legado** via GitHub (deprecated)
|
|
- [KB-PLUGIN-013](KB-PLUGIN-013-mindplace-plugin-publication-runbook.md) — Runbook completo de publicação no Mindplace via Forgejo
|
|
- [KB-PLUGIN-017](KB-PLUGIN-017-mindplace-zip-wrapper-detector-bug.md) — Bug do detector de wrapper (motivação histórica)
|