<?php namespace App\Models; use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use App\Models\Casts\{CompressedStringOrNull,CollectionOrNull}; class File extends Model { use SoftDeletes; private const LOGKEY = 'MF-'; private bool $no_export = FALSE; public Collection $set_path; public Collection $set_seenby; public string $src_file = ''; protected $casts = [ 'kludges' => CollectionOrNull::class, 'datetime' => 'datetime:Y-m-d H:i:s', 'desc' => CompressedStringOrNull::class, 'ldesc' => CompressedStringOrNull::class, 'rogue_seenby' => CollectionOrNull::class, 'rogue_path' => CollectionOrNull::class, 'size' => 'int', ]; public static function boot() { parent::boot(); static::creating(function($model) { if (! $model->filearea_id) { Log::alert(sprintf('%s:- File has no filearea, not processing creating [%s]',self::LOGKEY,$model->name)); return; } Log::info(sprintf('%s:- Storing file [%s] in [%s]',self::LOGKEY,$model->src_file,$model->rel_name)); $srcfs = Storage::disk(config('fido.local_disk')); $tgtfs = Storage::disk(config('fido.file_disk')); // Delete anything being replaced foreach (self::where('name',$model->name)->where('filearea_id',$model->filearea_id)->get() as $fo) { Log::info(sprintf('%s:%% Deleting old file record [%d] for file [%s]',self::LOGKEY,$fo->id,$fo->rel_name)); $tgtfs->move($fo->rel_name,$fo->relname.'.'.$fo->id); $fo->delete(); } // Store file if ($tgtfs->put($model->rel_name,$srcfs->get($model->src_file),'public')) { $srcfs->delete($model->src_file); } else { throw new \Exception(sprintf('Unable to move file [%s] to [%s]',$model->src_file,$model->rel_name)); } }); // @todo if the file is updated with new SEEN-BY's from another route, we'll delete the pending export for systems (if there is one) static::created(function($model) { if (! $model->filearea_id) { Log::alert(sprintf('%s:- File has no filearea, not exporting [%d]',self::LOGKEY,$model->id)); return; } $rogue = collect(); $seenby = collect(); $path = collect(); $zone = $model->fftn->zone; // Parse PATH /** * Path 21:4/106.0 @231005001126 PST+7 Foobar * Path 21:1/100 1696489954 Thu Oct 05 07:12:34 2023 UTC htick/lnx 1.9 2022-07-03 */ foreach ($model->set_path as $line) { $matches = []; preg_match(sprintf('#^(%s)\ ((@?)(\d+)(\ ([A-Z]{3}([\+\-][0-9]+)))?)\ ?(.*)$#',Address::ftn_regex),$line,$matches); // If our domain is blank, get the model's domain if (! Arr::get($matches,6)) $matches[1] .= '@'.$model->filearea->domain->name; if ($x=Arr::get($matches,1)) { $ftn = Address::parseFTN($x); // If domain should be flattened, look for node regardless of zone (within the list of zones for the domain) $ao = ($zone->domain->flatten) ? Address::findZone($zone->domain,$ftn['n'],$ftn['f'],0) : Address::findFTN($x); if (! $ao) $ao = Address::createFTN($x,System::createUnknownSystem()); $datetime = ($matches[8] === '@') ? Carbon::createFromFormat('ymdHis',$matches[9],$matches[12]) : Carbon::createFromTimestamp($matches[7]); $path->push(['address'=>$ao,'datetime'=>$datetime,'extra'=>$matches[13]]); } } // Save the Path $ppoid = NULL; foreach ($path as $item) { $po = DB::select('INSERT INTO file_path (file_id,address_id,parent_id,datetime,extra) VALUES (?,?,?,?,?) RETURNING id',[ $model->id, $item['address']->id, $ppoid, $item['datetime'], $item['extra'], ]); $ppoid = $po[0]->id; } // Make sure all the path is in the seenby // Add zone to seenby $model->set_seenby = $model->set_seenby->merge($path->pluck('address.ftn3d'))->unique()->filter(); foreach ($model->set_seenby as $sb) { if (! preg_match('/@([a-zA-Z0-9\-_~]{0,8})/',$sb)) $sb .= '@'.$model->filearea->domain->name; $ftn = Address::parseFTN($sb); $ao = ($zone->domain->flatten) ? Address::findZone($zone->domain,$ftn['n'],$ftn['f'],0) : Address::findFTN($sb); if ($ao) $seenby->push($ao->id); else $rogue->push($sb); } $model->rogue_seenby = $rogue; $model->seenby()->sync($seenby); $model->save(); // See if we need to export this file. if ($model->filearea->sec_read) { $exportto = $model ->filearea ->addresses ->filter(function($item) use ($model) { return $model->filearea->can_read($item->security); }) ->pluck('id') ->diff($seenby); if ($exportto->count()) { if ($model->no_export) { Log::alert(sprintf('%s:- NOT processing exporting of message by configuration [%s] to [%s]',self::LOGKEY,$model->id,$exportto->join(','))); return; } Log::info(sprintf('%s:- Exporting file [%s] to [%s]',self::LOGKEY,$model->id,$exportto->join(','))); // Save the seenby for the exported systems $model->seenby()->syncWithPivotValues($exportto,['export_at'=>Carbon::now()],FALSE); } } }); } /* RELATIONS */ public function filearea() { return $this->belongsTo(Filearea::class); } public function fftn() { return $this->belongsTo(Address::class) ->withTrashed(); } public function seenby() { return $this->belongsToMany(Address::class,'file_seenby') ->dontCache() ->ftnOrder(); } public function path() { return $this->belongsToMany(Address::class,'file_path') ->withPivot(['id','parent_id','datetime','extra']); } /* ATTRIBUTES */ public function getOriginAttribute(): Address { return $this->path->where('pivot.parent_id','=',NULL)->pop(); } /** * Return the relative path to Storage::disk() in the store * * @return string */ public function getRelNameAttribute(): string { return sprintf('%04X/%s',$this->filearea_id,$this->name); } /* METHODS */ public function jsonSerialize(): array { return $this->encode(); } }