Add Zmodem/BINKP/EMSI
This commit is contained in:
135
app/Classes/File/Item.php
Normal file
135
app/Classes/File/Item.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\File;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use League\Flysystem\UnreadableFileException;
|
||||
|
||||
/**
|
||||
* A file we are sending or receiving
|
||||
*
|
||||
* @property string $name
|
||||
* @property string $recvas
|
||||
* @property int $size
|
||||
*/
|
||||
class Item
|
||||
{
|
||||
protected const IS_PKT = (1<<1);
|
||||
protected const IS_ARC = (1<<2);
|
||||
protected const IS_FILE = (1<<3);
|
||||
protected const IS_FLO = (1<<4);
|
||||
protected const IS_REQ = (1<<5);
|
||||
|
||||
protected const I_RECV = (1<<6);
|
||||
protected const I_SEND = (1<<7);
|
||||
|
||||
protected string $file_name = '';
|
||||
protected int $file_size = 0;
|
||||
protected int $file_mtime = 0;
|
||||
protected int $file_type = 0;
|
||||
protected int $action = 0;
|
||||
|
||||
public bool $sent = FALSE;
|
||||
public bool $received = FALSE;
|
||||
|
||||
/**
|
||||
* @throws FileNotFoundException
|
||||
* @throws UnreadableFileException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct($file,int $action)
|
||||
{
|
||||
$this->action |= $action;
|
||||
|
||||
switch ($action) {
|
||||
case self::I_SEND:
|
||||
if (! is_string($file))
|
||||
throw new Exception('Invalid object creation - file should be a string');
|
||||
|
||||
if (! file_exists($file))
|
||||
throw new FileNotFoundException('Item doesnt exist: '.$file);
|
||||
|
||||
if (! is_readable($file))
|
||||
throw new UnreadableFileException('Item cannot be read: '.$file);
|
||||
|
||||
$this->file_name = $file;
|
||||
$x = stat($file);
|
||||
$this->file_size = $x['size'];
|
||||
$this->file_mtime = $x['mtime'];
|
||||
|
||||
break;
|
||||
|
||||
case self::I_RECV;
|
||||
$keys = ['name','mtime','size'];
|
||||
|
||||
if (! is_array($file) || array_diff(array_keys($file),$keys))
|
||||
throw new Exception('Invalid object creation - file is not a valid array :'.serialize(array_diff(array_keys($file),$keys)));
|
||||
|
||||
$this->file_name = $file['name'];
|
||||
$this->file_size = $file['size'];
|
||||
$this->file_mtime = $file['mtime'];
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Unknown action: '.$action);
|
||||
}
|
||||
|
||||
$this->file_type |= $this->whatType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
switch($key) {
|
||||
case 'mtime':
|
||||
case 'name':
|
||||
case 'size':
|
||||
if ($this->action & self::I_RECV)
|
||||
return $this->{'file_'.$key};
|
||||
|
||||
throw new Exception('Invalid request for key: '.$key);
|
||||
|
||||
case 'recvas':
|
||||
return '/tmp/'.$this->file_name; // @todo this should be inbound temp
|
||||
|
||||
case 'sendas':
|
||||
return basename($this->file_name);
|
||||
|
||||
default:
|
||||
throw new Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isType(int $type): bool
|
||||
{
|
||||
return $this->file_type & $type;
|
||||
}
|
||||
|
||||
private function whatType(): int
|
||||
{
|
||||
static $ext = ['su','mo','tu','we','th','fr','sa','req'];
|
||||
|
||||
$x = strrchr($this->file_name,'.');
|
||||
if (! $x || (strlen(substr($x,1)) != 3))
|
||||
return self::IS_FILE;
|
||||
|
||||
if (strcasecmp(substr($x,2),'lo') == 0)
|
||||
return self::IS_FLO;
|
||||
|
||||
if (strcasecmp(substr($x,1),'pkt') == 0)
|
||||
return self::IS_PKT;
|
||||
|
||||
if (strcasecmp(substr($x,1),'req') == 0)
|
||||
return self::IS_REQ;
|
||||
|
||||
for ($i=0;$i<count($ext);$i++)
|
||||
if (! strncasecmp($x,'.'.$ext[$i],strlen($ext[$i])) && (preg_match('/^[0-9a-z]/',strtolower(substr($x,3,1)))))
|
||||
return self::IS_ARC;
|
||||
|
||||
return self::IS_FILE;
|
||||
}
|
||||
}
|
173
app/Classes/File/Receive.php
Normal file
173
app/Classes/File/Receive.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\File;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
|
||||
/**
|
||||
* Object representing the files we are receiving
|
||||
*
|
||||
* @property-read resource $fd
|
||||
* @property-read int total_recv
|
||||
* @property-read int total_recv_bytes
|
||||
*/
|
||||
final class Receive extends Item
|
||||
{
|
||||
private Collection $list;
|
||||
private ?Item $receiving;
|
||||
|
||||
private mixed $f; // File descriptor
|
||||
private int $start; // Time we started receiving
|
||||
private int $file_pos; // Current write pointer
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Initialise our variables
|
||||
$this->list = collect();
|
||||
$this->receiving = NULL;
|
||||
$this->file_pos = 0;
|
||||
$this->f = NULL;
|
||||
}
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'fd':
|
||||
return is_resource($this->f);
|
||||
|
||||
case 'filepos':
|
||||
return $this->file_pos;
|
||||
|
||||
case 'mtime':
|
||||
case 'name':
|
||||
case 'size':
|
||||
return $this->receiving ? $this->receiving->{'file_'.$key} : NULL;
|
||||
|
||||
case 'to_get':
|
||||
return $this->list
|
||||
->filter(function($item) { return ($item->action & self::I_RECV) && $item->received === FALSE; })
|
||||
->count();
|
||||
|
||||
case 'total_recv':
|
||||
return $this->list
|
||||
->filter(function($item) { return ($item->action & self::I_RECV) && $item->received === TRUE; })
|
||||
->count();
|
||||
|
||||
case 'total_recv_bytes':
|
||||
return $this->list
|
||||
->filter(function($item) { return ($item->action & self::I_RECV) && $item->received === TRUE; })
|
||||
->sum(function($item) { return $item->file_size; });
|
||||
|
||||
default:
|
||||
throw new Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the file descriptor for our incoming file
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if (! $this->f)
|
||||
throw new Exception('No file to close');
|
||||
|
||||
if (! $this->file_pos != $this->receiving->file_size)
|
||||
Log::warning(sprintf('%s: - Closing [%s], but missing [%d] bytes',__METHOD__,$this->receiving->file_name,$this->receiving->file_size-$this->file_pos));
|
||||
|
||||
$this->receiving->received = TRUE;
|
||||
|
||||
$end = time()-$this->start;
|
||||
Log::debug(sprintf('%s: - Closing [%s], received in [%d]',__METHOD__,$this->receiving->file_name,$end));
|
||||
|
||||
fclose($this->f);
|
||||
$this->file_pos = 0;
|
||||
$this->receiving = NULL;
|
||||
$this->f = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the file descriptor to receive a file
|
||||
*
|
||||
* @param bool $check
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function open(bool $check=FALSE): bool
|
||||
{
|
||||
Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$check));
|
||||
|
||||
// Check we can open this file
|
||||
// @todo
|
||||
// @todo implement return 2 - SKIP file
|
||||
// @todo implement return 4 - SUSPEND(?) file
|
||||
if ($check) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (! $this->receiving)
|
||||
throw new Exception('No files currently receiving');
|
||||
|
||||
$this->file_pos = 0;
|
||||
$this->start = time();
|
||||
|
||||
Log::debug(sprintf('%s: - Opening [%s]',__METHOD__,$this->receiving->recvas));
|
||||
$this->f = fopen($this->receiving->recvas,'wb');
|
||||
if (! $this->f) {
|
||||
Log::error(sprintf('%s: ! Unable to open file [%s] for writing',__METHOD__,$this->receiving->file_name));
|
||||
return 3; // @todo change to const
|
||||
}
|
||||
|
||||
Log::info(sprintf('%s: = End - File [%s] opened for writing',__METHOD__,$this->receiving->file_name));
|
||||
return 0; // @todo change to const
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new file to receive
|
||||
*
|
||||
* @param array $file
|
||||
* @throws Exception
|
||||
*/
|
||||
public function new(array $file): void
|
||||
{
|
||||
Log::debug(sprintf('%s: + Start',__METHOD__),['file'=>$file]);
|
||||
|
||||
if ($this->receiving)
|
||||
throw new Exception('Can only have 1 file receiving at a time');
|
||||
|
||||
$o = new Item($file,self::I_RECV);
|
||||
$this->list->push($o);
|
||||
|
||||
$this->receiving = $o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to the file we are receiving
|
||||
*
|
||||
* @param string $buf
|
||||
* @return int
|
||||
* @throws Exception
|
||||
*/
|
||||
public function write(string $buf): int
|
||||
{
|
||||
if (! $this->f)
|
||||
throw new Exception('No file open for read');
|
||||
|
||||
if ($this->file_pos+strlen($buf) > $this->receiving->file_size)
|
||||
throw new Exception(sprintf('Too many bytes received [%d] (%d)?',$this->file_pos+strlen($buf),$this->receiving->file_size));
|
||||
|
||||
$rc = fwrite($this->f,$buf);
|
||||
|
||||
if ($rc === FALSE)
|
||||
throw new FileException('Error while writing to file');
|
||||
|
||||
$this->file_pos += $rc;
|
||||
Log::debug(sprintf('%s: - Write [%d] bytes, file pos now [%d] of [%d]',__METHOD__,$rc,$this->file_pos,$this->receiving->file_size));
|
||||
|
||||
return $rc;
|
||||
}
|
||||
}
|
235
app/Classes/File/Send.php
Normal file
235
app/Classes/File/Send.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\File;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use League\Flysystem\UnreadableFileException;
|
||||
|
||||
/**
|
||||
* 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 Collection $list;
|
||||
private ?Item $sending;
|
||||
|
||||
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->sending = NULL;
|
||||
$this->file_pos = 0;
|
||||
$this->f = NULL;
|
||||
}
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'fd':
|
||||
return is_resource($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();
|
||||
|
||||
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; });
|
||||
|
||||
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();
|
||||
|
||||
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; });
|
||||
|
||||
case 'total_count':
|
||||
return $this->list
|
||||
->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; });
|
||||
|
||||
default:
|
||||
throw new Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a file to the list of files to send
|
||||
*
|
||||
* @param string $file
|
||||
* @throws Exception
|
||||
*/
|
||||
public function add(string $file): void
|
||||
{
|
||||
Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$file));
|
||||
|
||||
try {
|
||||
$this->list->push(new Item($file,self::I_SEND));
|
||||
|
||||
} catch (FileNotFoundException) {
|
||||
Log::error(sprintf('%s: ! Item [%s] doesnt exist',__METHOD__,$file));
|
||||
return;
|
||||
|
||||
} catch (UnreadableFileException) {
|
||||
Log::error(sprintf('%s: ! Item [%s] cannot be read',__METHOD__,$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]',__METHOD__,$this->sending->file_name,$end));
|
||||
}
|
||||
|
||||
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 feof($this->f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a file for sending
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function open(): bool
|
||||
{
|
||||
Log::debug(sprintf('%s: + Start',__METHOD__));
|
||||
|
||||
$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',__METHOD__,$this->sending->file_name));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
Log::info(sprintf('%s: = End - File [%s] opened with size [%d]',__METHOD__,$this->sending->file_name,$this->sending->file_size));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read bytes of the sending file
|
||||
*
|
||||
* @param int $length
|
||||
* @return string|null
|
||||
* @throws UnreadableFileException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function read(int $length): ?string
|
||||
{
|
||||
if (! $this->f)
|
||||
throw new Exception('No file open for read');
|
||||
|
||||
$data = fread($this->f,$length);
|
||||
$this->file_pos += strlen($data);
|
||||
Log::debug(sprintf('%s: - Read [%d] bytes, file pos now [%d]',__METHOD__,strlen($data),$this->file_pos));
|
||||
|
||||
if ($data === FALSE)
|
||||
throw new UnreadableFileException('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');
|
||||
|
||||
$rc = (fseek($this->f,$pos,SEEK_SET) === 0);
|
||||
if ($rc)
|
||||
$this->file_pos = $pos;
|
||||
|
||||
Log::debug(sprintf('%s: - Seeked to [%d]',__METHOD__,$this->file_pos));
|
||||
|
||||
return $rc;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user