Skip to content

Commit

Permalink
enh: add files_scripts:interactive occ command
Browse files Browse the repository at this point in the history
  • Loading branch information
Raudius committed Oct 9, 2024
1 parent 22c9d5c commit 3d5f98d
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 28 deletions.
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Allows administrators to write small scripts which users can run through the fil
<command>OCA\FilesScripts\Command\ListScripts</command>
<command>OCA\FilesScripts\Command\ImportScripts</command>
<command>OCA\FilesScripts\Command\ExportScripts</command>
<command>OCA\FilesScripts\Command\Interactive</command>
</commands>
<settings>
<admin>OCA\FilesScripts\Settings\AdminSettings</admin>
Expand Down
13 changes: 13 additions & 0 deletions docs/Admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,16 @@ To get the `<userid>` value, you can use the user-list command:
```sh
occ user:list
```

## Interactive (REPL) shell
You can start an interactive shell with the `files_scripts:interactive` command
```sh
occ files_scripts:interactive
```

Multi-line commands can be given by ending the line with a backslash character:
```
> print("hello \
> world")
```
You can type the `exit` command to exit the shell.
90 changes: 90 additions & 0 deletions lib/Command/Interactive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
namespace OCA\FilesScripts\Command;

use OC\Core\Command\Base;
use OCA\FilesScripts\Db\Script;
use OCA\FilesScripts\Interpreter\Context;
use OCA\FilesScripts\Interpreter\Interpreter;
use OCA\FilesScripts\Interpreter\Lua\LuaProvider;
use OCP\Files\IRootFolder;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class Interactive extends Base {
private LuaProvider $luaProvider;
private IRootFolder $rootFolder;
private Interpreter $interpreter;

public function __construct(
IRootFolder $rootFolder,
LuaProvider $luaProvider,
Interpreter $interpreter
) {
parent::__construct('files_scripts:interactive');
$this->rootFolder = $rootFolder;
$this->luaProvider = $luaProvider;
$this->interpreter = $interpreter;
}
protected function configure(): void {
$this->setDescription('Starts an interactive Lua shell where you can interact with the server using the scripting API')
->addOption('user', 'u', InputOption::VALUE_OPTIONAL, 'User as which the action should be run')
->addOption('file', 'f', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'File path or id of a file given to the action as input file');

parent::configure();
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$userId = $input->getOption('user');
$rootFolder = $this->rootFolder;

try {
if ($userId) {
$rootFolder = $this->rootFolder->getUserFolder($userId);
}
$files = RunScript::getFilesForCommand($input, $output, $rootFolder);
} catch (\Throwable $e) {
$output->writeln('<error>' . $e->getMessage() .'</error>');
return 1;
}

$output->writeln('<info>Lua files_scripts interpreter started...</info>');
$output->writeln('<info>To stop type "exit"</info>');

$context = new Context($this->luaProvider->createLua(), $rootFolder, [], $files);
$f = fopen( 'php://stdin', 'r' );
$command = "";
while (true) {
echo "> ";
$line = fgets( $f );

// Handle exit clause
if (trim($line) == "exit") {
fclose($f);
break;
}

$replacements = 0;
$line = preg_replace('/(.*)\\\\(\s*)$/i', '$1 $2', $line, 1, $replacements);
$command .= $line;

// if line does not end with `\` backslash we execute the command
if ($replacements == 0) {
$this->executeCommand($command, $context, $output);
$command = "";
}
}

return 0;
}

private function executeCommand(string $command, Context $context, OutputInterface $output): void {
$script = new Script();
$script->setProgram($command);
try {
$this->interpreter->execute($script, $context);
} catch (\Throwable $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>');
}
}
}
69 changes: 42 additions & 27 deletions lib/Command/RunScript.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
<?php
namespace OCA\FilesScripts\Command;

use Error;
use OC\Core\Command\Base;
use OCA\FilesScripts\Db\ScriptInputMapper;
use OCA\FilesScripts\Db\ScriptMapper;
use OCA\FilesScripts\Interpreter\Context;
use OCA\FilesScripts\Interpreter\Lua\LuaProvider;
use OCA\FilesScripts\Service\ScriptService;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -44,29 +46,8 @@ protected function configure(): void {
parent::configure();
}

protected function execute(InputInterface $input, OutputInterface $output) {
$scriptId = $input->getArgument('id');
$userId = $input->getOption('user');
$scriptInputsJson = $input->getOption('inputs') ?? '{}';
public static function getFilesForCommand(InputInterface $input, OutputInterface $output, Folder $rootFolder) {
$fileInputs = $input->getOption('file') ?? [];

try {
$scriptInputsData = json_decode($scriptInputsJson, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $err) {
$output->writeln('<error>Could not parse the inputs JSON</error>');
return 1;
}

$script = $this->scriptMapper->find($scriptId);
$output->writeln('<info>Executing file action: ' . $script->getTitle() . '</info>');

$scriptInputs = $this->scriptInputMapper->findAllByScriptId($scriptId);
foreach ($scriptInputs as $scriptInput) {
$value = $scriptInputsData[$scriptInput->getName()] ?? null;
$scriptInput->setValue($value);
}

$rootFolder = $this->rootFolder->getUserFolder($userId);

$files = [];
$n = 1;
Expand All @@ -77,22 +58,56 @@ protected function execute(InputInterface $input, OutputInterface $output) {
if (ctype_digit(strval($fileInput))) {
$nodes = $rootFolder->getById(intval($fileInput));
if (!isset($nodes[0])) {
$output->writeln('<error>Could not find input file ' . $fileInput . ' belonging in root folder ' . $rootFolder->getPath() . ' for file action</error>');
return 1;
throw new Error('Could not find input file ' . $fileInput . ' belonging in root folder ' . $rootFolder->getPath() . ' for file action');
}
$file = $nodes[0];
unset($nodes);
} else {
$file = $rootFolder->get($fileInput);
}
} catch (\Exception $e) {
$output->writeln('<error>Could not find input file ' . $fileInput . ' belonging in root folder ' . $rootFolder->getPath() . ' for file action</error>');
return 1;
} catch (\Throwable $e) {
throw $e;
}
$files[$n++] = $file;
}
}

return $files;
}

protected function execute(InputInterface $input, OutputInterface $output) {
$scriptId = $input->getArgument('id');
$userId = $input->getOption('user');
$scriptInputsJson = $input->getOption('inputs') ?? '{}';

try {
$scriptInputsData = json_decode($scriptInputsJson, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $err) {
$output->writeln('<error>Could not parse the inputs JSON</error>');
return 1;
}

$script = $this->scriptMapper->find($scriptId);
$output->writeln('<info>Executing file action: ' . $script->getTitle() . '</info>');

$scriptInputs = $this->scriptInputMapper->findAllByScriptId($scriptId) ?? [];
foreach ($scriptInputs as $scriptInput) {
$value = $scriptInputsData[$scriptInput->getName()] ?? null;
$scriptInput->setValue($value);
}

$rootFolder = $this->rootFolder;
try {
if ($userId) {
$rootFolder = $this->rootFolder->getUserFolder($userId);
}
$files = self::getFilesForCommand($input, $output, $rootFolder);
} catch (\Throwable $e) {
$output->writeln('<error>' . $e->getMessage() .'</error>');
return 1;
}

print(gettype($scriptInputs));
$context = new Context(
$this->luaProvider->createLua(),
$rootFolder,
Expand Down
2 changes: 1 addition & 1 deletion lib/Interpreter/RegistrableFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ final protected function getHomeFolder(): Folder {
return $folder;
}

final protected function getPath(array $data): string {
final protected function getPath(?array $data): string {
return ($data['path'] ?? '<no-path>') . '/' . ($data['name'] ?? '<no-name>');
}

Expand Down

0 comments on commit 3d5f98d

Please sign in to comment.