AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Nextcloud Impl File Operations

skill-impertio-studio-nextcloud-claude-skill-package-nextcloud-impl-file-operations · by Impertio-Studio

>

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-impertio-studio-nextcloud-claude-skill-package-nextcloud-impl-file-operations

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-impertio-studio-nextcloud-claude-skill-package-nextcloud-impl-file-operations)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Nextcloud Impl File Operations? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

nextcloud-impl-file-operations

> Implementation workflows for Nextcloud file operations. For API signatures and method tables, see [nextcloud-syntax-file-api](../../nextcloud-syntax/nextcloud-syntax-file-api/SKILL.md).

Quick Reference

Workflow Map

| Workflow | When to Use | Key Pattern | |----------|-------------|-------------| | File CRUD Service | App needs to read/write user files | Service class with IRootFolder injection | | Folder Tree Management | App organizes files in directories | Recursive ensure-folder + upsert | | File Search | App finds files by criteria | ISearchQuery with SearchComparison | | Event-Driven Processing | React to file changes | Event listeners in register() | | Batch File Operations | Process many files at once | Stream-based + chunked iteration | | Trash & Versioning | Integrate with file history | ITrashManager / IVersionManager |

Service Architecture

Controller → FileService → IRootFolder → getUserFolder($userId)
                ↓                              ↓
           Business logic              Node API (File/Folder)
                ↓                              ↓
           Return DTO                  Events dispatched automatically

ALWAYS build a dedicated service class for file operations -- NEVER put file logic directly in controllers.

ALWAYS pass $userId as a parameter to service methods -- NEVER rely on session state inside services.


Decision Tree: Choosing a File Workflow

What does your app need?
├── CRUD on individual files?
│   ├── Simple read/write → Upsert Pattern (Pattern 1)
│   ├── Large files (>5MB) → Stream Pattern (Pattern 4)
│   └── Files by ID → Safe getById Pattern (Pattern 2)
├── Organize files in folders?
│   ├── Single level → nodeExists() + newFolder()
│   └── Deep nesting → Recursive Ensure Pattern (Pattern 3)
├── Find files matching criteria?
│   ├── By name/mime → SearchQuery Pattern (Pattern 5)
│   └── By tag → searchByTag()
├── React to file changes?
│   ├── After change → NodeCreatedEvent / NodeWrittenEvent
│   ├── Before change (block) → BeforeNodeDeletedEvent + throw
│   └── Favorites → NodeAddedToFavorite / NodeRemovedFromFavorite
├── Integrate with trash/versions?
│   ├── Restore from trash → ITrashManager (Pattern 7)
│   └── Access file versions → IVersionManager (Pattern 8)
└── Direct storage access?
    └── ONLY when Node API is insufficient → Pattern 9

Essential Patterns

Pattern 1: File CRUD Service with Upsert

ALWAYS use this pattern as the foundation for any file-manipulating app.

namespace OCA\MyApp\Service;

use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use Psr\Log\LoggerInterface;

class FileOperationService {
    public function __construct(
        private IRootFolder $rootFolder,
        private LoggerInterface $logger,
    ) {}

    /**
     * Create or update a file. ALWAYS use this instead of
     * separate create/update methods to avoid race conditions.
     */
    public function upsertFile(string $userId, string $path, string $content): File {
        $userFolder = $this->rootFolder->getUserFolder($userId);

        try {
            $node = $userFolder->get($path);
            if (!($node instanceof File)) {
                throw new \RuntimeException('Path exists but is not a file: ' . $path);
            }
            $node->putContent($content);
            return $node;
        } catch (NotFoundException) {
            // Ensure parent directory exists
            $parentPath = dirname($path);
            if ($parentPath !== '.' && $parentPath !== '/') {
                $this->ensureFolderExists($userId, $parentPath);
            }
            return $userFolder->newFile($path, $content);
        }
    }

    public function readFile(string $userId, string $path): string {
        $userFolder = $this->rootFolder->getUserFolder($userId);

        try {
            $node = $userFolder->get($path);
        } catch (NotFoundException) {
            throw new NotFoundException('File not found: ' . $path);
        }

        if (!($node instanceof File)) {
            throw new \RuntimeException('Path is not a file: ' . $path);
        }

        return $node->getContent();
    }

    public function deleteFile(string $userId, string $path): void {
        $userFolder = $this->rootFolder->getUserFolder($userId);

        try {
            $node = $userFolder->get($path);
            $node->delete();
        } catch (NotFoundException) {
            $this->logger->debug('Delete skipped, file already gone: ' . $path);
        }
    }

    public function ensureFolderExists(string $userId, string $path): Folder {
        $userFolder = $this->rootFolder->getUserFolder($userId);
        $parts = explode('/', trim($path, '/'));
        $current = $userFolder;

        foreach ($parts as $part) {
            if ($current->nodeExists($part)) {
                $node = $current->get($part);
                if (!($node instanceof Folder)) {
                    throw new \RuntimeException('Path component is a file: ' . $part);
                }
                $current = $node;
            } else {
                $current = $current->newFolder($part);
            }
        }

        return $current;
    }
}

Pattern 2: Safe File Lookup by ID

ALWAYS use this pattern when resolving file IDs from frontend or API requests.

public function getFileById(string $userId, int $fileId): File {
    $userFolder = $this->rootFolder->getUserFolder($userId);
    $nodes = $userFolder->getById($fileId);

    if (empty($nodes)) {
        throw new NotFoundException('File not found for ID: ' . $fileId);
    }

    $node = $nodes[0];
    if (!($node instanceof File)) {
        throw new \RuntimeException('Node is not a file');
    }

    return $node;
}

NEVER assume getById() returns a single node -- it returns an array because files can exist in multiple mount points.

Pattern 3: Controller Integration

ALWAYS delegate to a service -- controllers handle HTTP concerns only.

namespace OCA\MyApp\Controller;

use OCA\MyApp\Service\FileOperationService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http;
use OCP\Files\NotFoundException;
use OCP\IRequest;

class FileController extends Controller {
    public function __construct(
        string $appName,
        IRequest $request,
        private FileOperationService $fileService,
        private ?string $userId,
    ) {
        parent::__construct($appName, $request);
    }

    #[\OCP\AppFramework\Http\Attribute\NoAdminRequired]
    public function read(string $path): JSONResponse {
        try {
            $content = $this->fileService->readFile($this->userId, $path);
            return new JSONResponse(['content' => $content]);
        } catch (NotFoundException) {
            return new JSONResponse(
                ['error' => 'File not found'],
                Http::STATUS_NOT_FOUND
            );
        }
    }

    #[\OCP\AppFramework\Http\Attribute\NoAdminRequired]
    public function save(string $path, string $content): JSONResponse {
        $file = $this->fileService->upsertFile($this->userId, $path, $content);
        return new JSONResponse(['id' => $file->getId()]);
    }
}

Pattern 4: Event-Driven File Processing

ALWAYS register event listeners in register(), NEVER in boot().

// lib/AppInfo/Application.php
public function register(IRegistrationContext $context): void {
    $context->registerEventListener(
        NodeWrittenEvent::class,
        FileChangedListener::class
    );
    $context->registerEventListener(
        BeforeNodeDeletedEvent::class,
        ProtectFileListener::class
    );
}
// lib/Listener/FileChangedListener.php
namespace OCA\MyApp\Listener;

use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\Node\NodeWrittenEvent;
use OCP\Files\File;

/** @template-implements IEventListener */
class FileChangedListener implements IEventListener {
    public function __construct(
        private MyProcessingService $processor,
    ) {}

    public function handle(Event $event): void {
        if (!($event instanceof NodeWrittenEvent)) {
            return;
        }

        $node = $event->getNode();
        if (!($node instanceof File)) {
            return;
        }

        // ALWAYS filter early to avoid unnecessary processing
        if ($node->getMimeType() !== 'application/json') {
            return;
        }

        $this->processor->processJsonFile($node);
    }
}

Pattern 5: Blocking Deletion with Before Events

/** @template-implements IEventListener */
class ProtectFileListener implements IEventListener {
    public function handle(Event $event): void {
        if (!($event instanceof BeforeNodeDeletedEvent)) {
            return;
        }

        $node = $event->getNode();

        if ($this->isProtected($node)) {
            throw new \OCP\Files\ForbiddenException(
                'Protected files cannot be deleted',
                false
            );
        }
    }
}

ALWAYS throw ForbiddenException from Before-events to block the operation. NEVER throw generic exceptions.

Pattern 6: File Search Workflow

use OCP\Files\Search\ISearchComparison;
use OCP\Files\Search\SearchComparison;
use OCP\Files\Search\SearchQuery;

public function findFiles(string $userId, string $namePattern, int $limit = 50): array {
    $userFolder = $this->rootFolder->getUserFolder($userId);

    $query = new SearchQuery(
        new SearchComparison(
            ISearchComparison::COMPARE_LIKE,
            'name',
            '%' . $namePattern . '%'
        ),
        limit: $limit,
        offset: 0
    );

    $results = $userFolder->search($query);

    return array_map(fn($node) => [
        'id' => $node->getId(),
        'name' => $node->getName(),
        'path' => $userFolder->getRelativePath($node->getPath()),
        'size' => $node->getSize(),
        'mtime' => $node->getMTime(),
    ], $results);
}

ALWAYS set a limit on search queries -- unbounded searches on large installations cause timeouts.


Critical Rules

  1. ALWAYS inject IRootFolder via constructor DI -- NEVER instantiate or inject IUserFolder directly.
  2. ALWAYS wrap file access in try/catch for NotFoundException -- files can vanish between check and access.
  3. ALWAYS check instanceof File or instanceof Folder after get() -- the return type is Node.
  4. ALWAYS use fopen() for files larger than 5MB -- getContent() loads entire file into memory.
  5. ALWAYS use full internal paths for move() and copy(): $userFolder->getPath() . '/' . $target.
  6. NEVER put file operation logic directly in controllers -- delegate to a service class.
  7. NEVER use direct storage access ($file->getStorage()) for standard CRUD -- the Node API handles events, caching, encryption, and versioning automatically.
  8. NEVER register file event listeners in boot() -- ALWAYS use register() to avoid missed events.
  9. NEVER create files without checking existence first -- newFile() throws NotPermittedException on duplicates.
  10. NEVER forget to close file handles from fopen() -- ALWAYS use try/finally.

Reference Links

  • [references/methods.md](references/methods.md) -- Complete File, Folder, IRootFolder method reference for implementation
  • [references/examples.md](references/examples.md) -- CRUD workflows, search, event handling, trash, versioning
  • [references/anti-patterns.md](references/anti-patterns.md) -- File operation implementation mistakes

Related Skills

  • [nextcloud-syntax-file-api](../../nextcloud-syntax/nextcloud-syntax-file-api/SKILL.md) -- API signatures and method tables

Official Sources

  • https://docs.nextcloud.com/server/latest/developermanual/diggingdeeper/files.html
  • https://docs.nextcloud.com/server/latest/developermanual/diggingdeeper/events.html
  • https://docs.nextcloud.com/server/latest/developer_manual/basics/storage.html

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.