Files
2026-10-23 18:46:50 +02:00

214 lines
8.2 KiB
C#

using Omika.App.Local.Abstractions;
using Omika.App.Local.Models;
using Omika.Core.Contracts.Mobile.Languages;
namespace Omika.LanguesProbe;
/// <summary>
/// Magasin de pack de langue adossé à un dossier du disque : c'est ce qui permet
/// d'exécuter le <b>vrai</b> pipeline (<c>OnnxTranslationService</c>) sur les
/// fichiers d'un développeur, hors téléphone.
///
/// <para>
/// Le dossier passé en ligne de commande est traité comme
/// « <c>langues/&lt;code&gt;/&lt;sens&gt;</c> » : tous les fichiers
/// <c>*.spm</c> et <c>*.onnx</c> qu'il contient sont déclarés pour le sens
/// demandé, avec leur taille réelle. Aucune empreinte SHA-256 n'est inventée :
/// un pack décrit depuis le disque n'a pas de référence serveur à laquelle se
/// comparer, et une fausse empreinte rendrait la vérification mensongère.
/// </para>
///
/// <para>
/// <b>Lecture seule.</b> Les méthodes d'écriture de l'interface refusent d'agir
/// plutôt que de faire semblant : un outil de diagnostic ne doit jamais
/// modifier, ni supprimer, ce qu'un développeur lui demande d'examiner.
/// </para>
/// </summary>
internal sealed class PackLangueDossier : ILanguagePackStore
{
private readonly string _dossier;
private readonly string _code;
private readonly string _sens;
public PackLangueDossier(string dossier, string code, string sens)
{
_dossier = dossier;
_code = code;
_sens = sens;
}
/// <summary>
/// Dossier partagé entre les deux sens d'une même langue
/// (« &lt;code&gt;\commun »), frère du dossier examiné. Un modèle familial
/// (<c>opus-mt-tc-big-itc-itc</c>) y range ses poids une seule fois, et chaque
/// sens ne porte que son <c>modele.json</c>. Absent pour un pack bilingue.
/// </summary>
private string DossierCommun
{
get
{
string? parent = Path.GetDirectoryName(_dossier);
return string.IsNullOrEmpty(parent)
? string.Empty
: Path.Combine(parent, LanguageModelDirections.Commun);
}
}
/// <summary>Répertoire racine des packs sur l'appareil — ici, le dossier examiné.</summary>
public string Racine => _dossier;
/// <summary>
/// Aucun manifeste serveur n'est disponible hors application : le retour
/// <see langword="null"/> est l'état « mémoire d'index absente », déjà géré
/// par les appelants.
/// </summary>
public Task<LanguageManifestResponse?> GetIndexAsync(CancellationToken cancellationToken = default)
=> Task.FromResult<LanguageManifestResponse?>(null);
public Task EnregistrerIndexAsync(LanguageManifestResponse manifeste, CancellationToken cancellationToken = default)
=> throw LectureSeule();
public Task<PackLangueInstalle?> GetPackInstalleAsync(string code, CancellationToken cancellationToken = default)
=> Task.FromResult<PackLangueInstalle?>(DecrirePack());
public Task<IReadOnlyList<PackLangueInstalle>> ListerPacksInstallesAsync(CancellationToken cancellationToken = default)
{
var packs = new List<PackLangueInstalle>();
if (Directory.Exists(_dossier))
{
packs.Add(DecrirePack());
}
return Task.FromResult<IReadOnlyList<PackLangueInstalle>>(packs);
}
public Task EnregistrerPackAsync(PackLangueInstalle pack, CancellationToken cancellationToken = default)
=> throw LectureSeule();
public Task SupprimerPackAsync(string code, CancellationToken cancellationToken = default)
=> throw LectureSeule();
public string PreparerFichierPartiel(string code, string sens, string nomFichier)
=> throw LectureSeule();
/// <summary>
/// Décrit ce qui est <b>réellement présent</b> : c'est cette description qui
/// décide des fichiers que le service ouvrira, donc ce que l'outil mesure.
/// </summary>
public PackLangueInstalle DecrirePack()
{
var fichiers = new List<FichierModeleInstalle>();
if (Directory.Exists(_dossier))
{
// Tri par nom : deux exécutions successives doivent produire exactement le même
// inventaire, sans quoi une comparaison entre deux mesures n'aurait aucun sens.
string[] chemins = Directory.GetFiles(_dossier);
Array.Sort(chemins, StringComparer.Ordinal);
foreach (string chemin in chemins)
{
string nom = Path.GetFileName(chemin);
if (!nom.EndsWith(".spm", StringComparison.OrdinalIgnoreCase)
&& !nom.EndsWith(".onnx", StringComparison.OrdinalIgnoreCase))
{
continue;
}
fichiers.Add(new FichierModeleInstalle
{
Sens = _sens,
NomFichier = nom,
TailleOctets = new FileInfo(chemin).Length
});
}
}
// Poids partagés d'un modèle familial : le service les résout APRÈS le
// dossier du sens (voir OnnxTranslationService.FichiersDuSens). Les
// omettre ferait déclarer « pack incomplet » un pack familial valide.
string commun = DossierCommun;
if (commun.Length > 0 && Directory.Exists(commun))
{
string[] cheminsCommuns = Directory.GetFiles(commun);
Array.Sort(cheminsCommuns, StringComparer.Ordinal);
foreach (string chemin in cheminsCommuns)
{
string nom = Path.GetFileName(chemin);
if (!nom.EndsWith(".spm", StringComparison.OrdinalIgnoreCase)
&& !nom.EndsWith(".onnx", StringComparison.OrdinalIgnoreCase))
{
continue;
}
fichiers.Add(new FichierModeleInstalle
{
Sens = LanguageModelDirections.Commun,
NomFichier = nom,
TailleOctets = new FileInfo(chemin).Length
});
}
}
return new PackLangueInstalle
{
Code = _code,
// Le libellé vient du manifeste serveur, absent ici ; il n'est lu par aucun appel du
// pipeline de traduction.
Libelle = _code,
Version = string.Empty,
InstalleLeUtc = DateTimeOffset.UtcNow,
Fichiers = fichiers
};
}
public bool FichierPresent(string code, FichierModeleInstalle fichier)
=> File.Exists(CheminFichier(code, fichier.Sens, fichier.NomFichier));
/// <summary>
/// Vérifie la présence et la taille relevée à l'inventaire. L'empreinte
/// SHA-256 n'est pas recalculée : le pack étant décrit depuis le disque
/// lui-même, il n'existe aucune empreinte attendue à confronter.
/// </summary>
public Task<bool> VerifierFichierAsync(
string code, FichierModeleInstalle fichier, CancellationToken cancellationToken = default)
{
var info = new FileInfo(CheminFichier(code, fichier.Sens, fichier.NomFichier));
bool conforme = info.Exists && (fichier.TailleOctets <= 0 || info.Length == fichier.TailleOctets);
return Task.FromResult(conforme);
}
/// <summary>
/// Chemin réel d'un fichier du pack. Le nom est réduit à son nom simple,
/// comme dans <c>LanguagePaths</c> : un nom de fichier ne doit jamais
/// permettre de sortir du dossier examiné. Un fichier absent du dossier du
/// sens est cherché dans son frère <c>commun</c> (poids partagé d'un modèle
/// familial), en suivant exactement la priorité du service.
/// </summary>
public string CheminFichier(string code, string sens, string nomFichier)
{
string nom = Path.GetFileName(nomFichier);
string chemin = Path.Combine(_dossier, nom);
if (File.Exists(chemin))
{
return chemin;
}
string commun = DossierCommun;
if (commun.Length > 0)
{
string partage = Path.Combine(commun, nom);
if (File.Exists(partage))
{
return partage;
}
}
return chemin;
}
private static NotSupportedException LectureSeule()
=> new("Cet outil de diagnostic ne modifie jamais le dossier examiné.");
}