# Nextcloud Impl File Operations

> >

- **Type:** Skill
- **Install:** `agentstack add skill-impertio-studio-nextcloud-claude-skill-package-nextcloud-impl-file-operations`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Impertio-Studio](https://agentstack.voostack.com/s/impertio-studio)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Impertio-Studio](https://github.com/Impertio-Studio)
- **Source:** https://github.com/Impertio-Studio/Nextcloud-Claude-Skill-Package/tree/main/skills/source/nextcloud-impl/nextcloud-impl-file-operations

## Install

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

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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.

```php
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.

```php
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.

```php
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()`.

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

```php
// 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

```php
/** @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

```php
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/developer_manual/digging_deeper/files.html
- https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/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.

- **Author:** [Impertio-Studio](https://github.com/Impertio-Studio)
- **Source:** [Impertio-Studio/Nextcloud-Claude-Skill-Package](https://github.com/Impertio-Studio/Nextcloud-Claude-Skill-Package)
- **License:** MIT

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

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-impertio-studio-nextcloud-claude-skill-package-nextcloud-impl-file-operations
- Seller: https://agentstack.voostack.com/s/impertio-studio
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
