<?php namespace App\Classes\File; use Exception; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use League\Flysystem\UnreadableFileEncountered; use App\Models\Address; /** * Object representing the files we are sending * * @property-read resource fd * @property-read int file_mtime * @property-read int file_size * @property-read string file_name * @property-read int mail_size * @property-read int total_count * @property-read int total_sent * @property-read int total_sent_bytes */ final class Send extends Item { private const LOGKEY = 'IS-'; private Collection $list; private ?Item $sending; private Collection $packets; private mixed $f; // File descriptor private int $start; // Time we started sending private int $file_pos; // Current read pointer public function __construct() { // Initialise our variables $this->list = collect(); $this->packets = collect(); $this->sending = NULL; $this->file_pos = 0; $this->f = NULL; } public function __get($key) { switch ($key) { case 'fd': return is_resource($this->f) ?: $this->f; case 'file_count': return $this->list ->filter(function($item) { return $item->isType(self::IS_FILE); }) ->count(); case 'file_size': return $this->list ->filter(function($item) { return $item->isType(self::IS_FILE); }) ->sum(function($item) { return $item->file_size; }); case 'filepos': return $this->file_pos; case 'mail_count': return $this->list ->filter(function($item) { return $item->isType(self::IS_ARC|self::IS_PKT); }) ->count() + $this->packets->count(); case 'mail_size': return $this->list ->filter(function($item) { return $item->isType(self::IS_ARC|self::IS_PKT); }) ->sum(function($item) { return $item->file_size; }) + $this->packets->sum(function($item) { return $item->file_size; }); case 'sendas': return $this->sending ? $this->sending->{$key} : NULL; case 'name': case 'mtime': case 'size': return $this->sending ? $this->sending->{'file_'.$key} : NULL; case 'total_sent': return $this->list ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) ->count() + $this->packets ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) ->count(); case 'total_sent_bytes': return $this->list ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) ->sum(function($item) { return $item->file_size; }) + $this->packets ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) ->sum(function($item) { return $item->file_size; }); case 'total_count': return $this->list ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) ->count() + $this->packets ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) ->count(); case 'total_size': return $this->list ->sum(function($item) { return $item->file_size; }) + $this->packets->sum(function($item) { return $item->file_size; }); default: throw new Exception('Unknown key: '.$key); } } /** * Add a file to the list of files to send * * @param string $file * @throws Exception * @todo Catch if we add the same file twice */ public function add(string $file): void { Log::debug(sprintf('%s:+ add [%s]',self::LOGKEY,$file)); try { $this->list->push(new Item($file,self::I_SEND)); } catch (FileNotFoundException) { Log::error(sprintf('%s:! Item [%s] doesnt exist',self::LOGKEY,$file)); return; } catch (UnreadableFileEncountered) { Log::error(sprintf('%s:! Item [%s] cannot be read',self::LOGKEY,$file)); return; // Uncaught, rethrow the error } catch (Exception $e) { throw new Exception($e->getMessage()); } } /** * Close the file descriptor of the file we are sending * * @param bool $successful * @throws Exception */ public function close(bool $successful): void { if (! $this->f) throw new Exception('No file to close'); if ($successful) { $this->sending->sent = TRUE; $end = time()-$this->start; Log::debug(sprintf('%s: - Closing [%s], sent in [%d]',self::LOGKEY,$this->sending->file_name,$end)); } if (! $this->sending instanceof Mail) fclose($this->f); $this->sending = NULL; $this->file_pos = 0; $this->f = NULL; } /** * Check if we are at the end of the file * * @return bool */ public function feof(): bool { return ($this->sending instanceof Mail) ? ($this->file_pos == $this->size) : feof($this->f); } /** * Open a file for sending * * @return bool * @throws Exception */ public function open(): bool { Log::debug(sprintf('%s:+ open',self::LOGKEY)); // If we have mail, we'll send that first if ($this->sending = $this->packets ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) ->first()) { $this->file_pos = 0; $this->start = time(); $this->f = TRUE; return TRUE; } $this->sending = $this->list ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) ->first(); if (! $this->sending) throw new Exception('No files to open'); $this->file_pos = 0; $this->start = time(); $this->f = fopen($this->sending->file_name,'rb'); if (! $this->f) { Log::error(sprintf('%s:! Unable to open file [%s] for reading',self::LOGKEY,$this->sending->file_name)); return FALSE; } Log::info(sprintf('%s:= open - File [%s] opened with size [%d]',self::LOGKEY,$this->sending->file_name,$this->sending->file_size)); return TRUE; } /** * Add our mail to the send queue * * @param Address $ao * @throws Exception * @todo We need to make this into a transaction, incase the transfer fails. */ public function mail(Address $ao): void { // If the node is marked as hold - dont send any mail. if ($ao->system->hold) { Log::info(sprintf('%s: - System [%d] mail is marked as hold - not checking for mail.',self::LOGKEY,$ao->system_id)); return; } // Netmail if ($x=$ao->getNetmail()) { Log::debug(sprintf('%s: - Netmail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn)); $this->packets->push(new Mail($x,self::I_SEND)); } // Echomail if ($x=$ao->getEchomail()) { Log::debug(sprintf('%s: - Echomail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn)); $this->packets->push(new Mail($x,self::I_SEND)); } } /** * Read bytes of the sending file * * @param int $length * @return string|null * @throws UnreadableFileEncountered * @throws Exception */ public function read(int $length): ?string { if (! $this->f) throw new Exception('No file open for read'); // We are sending mail if ($this->sending instanceof Mail) { $data = $this->sending->read($this->file_pos,$length); } else { $data = fread($this->f,$length); } $this->file_pos += strlen($data); Log::debug(sprintf('%s: - Read [%d] bytes, file pos now [%d]',self::LOGKEY,strlen($data),$this->file_pos)); if ($data === FALSE) throw new UnreadableFileEncountered('Error reading file: '.$this->sending->file_name); return $data; } /** * Seek to a specific position of our file * * @param int $pos * @return bool * @throws Exception */ public function seek(int $pos): bool { if (! $this->f) throw new Exception('No file open for seek'); if ($this->sending instanceof Mail) { $pos = ($pos < $this->size) ? $pos : $this->size; $rc = TRUE; } else { $rc = (fseek($this->f,$pos,SEEK_SET) === 0); } if ($rc) $this->file_pos = $pos; Log::debug(sprintf('%s:= Seeked to [%d]',self::LOGKEY,$this->file_pos)); return $rc; } }