using Omika.App.Local.Abstractions;
using Omika.App.Local.Models;
using Omika.Core.Contracts.Mobile.Languages;
namespace Omika.LanguesProbe;
///
/// Magasin de pack de langue adossé à un dossier du disque : c'est ce qui permet
/// d'exécuter le vrai pipeline (OnnxTranslationService) sur les
/// fichiers d'un développeur, hors téléphone.
///
///
/// Le dossier passé en ligne de commande est traité comme
/// « langues/<code>/<sens> » : tous les fichiers
/// *.spm et *.onnx 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.
///
///
///
/// Lecture seule. 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.
///
///
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;
}
///
/// Dossier partagé entre les deux sens d'une même langue
/// (« <code>\commun »), frère du dossier examiné. Un modèle familial
/// (opus-mt-tc-big-itc-itc) y range ses poids une seule fois, et chaque
/// sens ne porte que son modele.json. Absent pour un pack bilingue.
///
private string DossierCommun
{
get
{
string? parent = Path.GetDirectoryName(_dossier);
return string.IsNullOrEmpty(parent)
? string.Empty
: Path.Combine(parent, LanguageModelDirections.Commun);
}
}
/// Répertoire racine des packs sur l'appareil — ici, le dossier examiné.
public string Racine => _dossier;
///
/// Aucun manifeste serveur n'est disponible hors application : le retour
/// est l'état « mémoire d'index absente », déjà géré
/// par les appelants.
///
public Task GetIndexAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(null);
public Task EnregistrerIndexAsync(LanguageManifestResponse manifeste, CancellationToken cancellationToken = default)
=> throw LectureSeule();
public Task GetPackInstalleAsync(string code, CancellationToken cancellationToken = default)
=> Task.FromResult(DecrirePack());
public Task> ListerPacksInstallesAsync(CancellationToken cancellationToken = default)
{
var packs = new List();
if (Directory.Exists(_dossier))
{
packs.Add(DecrirePack());
}
return Task.FromResult>(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();
///
/// Décrit ce qui est réellement présent : c'est cette description qui
/// décide des fichiers que le service ouvrira, donc ce que l'outil mesure.
///
public PackLangueInstalle DecrirePack()
{
var fichiers = new List();
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));
///
/// 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.
///
public Task 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);
}
///
/// Chemin réel d'un fichier du pack. Le nom est réduit à son nom simple,
/// comme dans LanguagePaths : 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 commun (poids partagé d'un modèle
/// familial), en suivant exactement la priorité du service.
///
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é.");
}