nextcloud

package
v0.0.0-...-46fd052 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: AGPL-3.0 Imports: 27 Imported by: 0

Documentation

Overview

Package nextcloud is a client library for NextCloud. It only supports files via Webdav for the moment.

Index

Constants

View Source
const (
	MigrationStatusPending   = "pending"
	MigrationStatusRunning   = "running"
	MigrationStatusCompleted = "completed"
	MigrationStatusFailed    = "failed"
	MigrationStatusCanceled  = "canceled"
)
View Source
const DefaultMigrationTargetDir = "/Nextcloud"

Variables

View Source
var (
	// ErrAccountNotFound is used when the no account can be found with the
	// given ID.
	ErrAccountNotFound = errors.New("account not found")
	// ErrInvalidAccount is used when the account cannot be used to connect to
	// NextCloud.
	ErrInvalidAccount = errors.New("invalid NextCloud account")
)
View Source
var ErrMigrationAlreadyTerminal = errors.New("nextcloud migration already in a terminal state")

ErrMigrationAlreadyTerminal is returned when cancel is requested on a migration that has already reached completed, failed, or canceled. Callers map it to a 409. The migration service's cancel_requested fallback would swallow a stray message, but publishing when there is nothing to stop wastes broker work and pollutes metrics.

View Source
var ErrMigrationBrokerUnavailable = errors.New("migration broker unavailable")

ErrMigrationBrokerUnavailable is returned when the RabbitMQ publish fails after the tracking document has been created. The tracking document is marked failed before this error is returned, so retries are not blocked by a stuck pending doc.

View Source
var ErrMigrationConflict = errors.New("a nextcloud migration is already in progress")
View Source
var ErrMigrationNotDeletable = errors.New("nextcloud migration source can only be deleted after a completed migration")

ErrMigrationNotDeletable gates delete-source to the completed status. Failed and canceled migrations may still hold files on Nextcloud that never reached Cozy, and removing the folder would lose them.

View Source
var ErrMigrationNotFound = errors.New("nextcloud migration not found")

ErrMigrationNotFound is returned when the tracking document referenced by a cancel request does not exist. Callers map it to a 404 rather than publishing into the void.

View Source
var ErrMigrationSourceAlreadyDeleted = errors.New("nextcloud migration source already deleted")

ErrMigrationSourceAlreadyDeleted is surfaced rather than swallowed so duplicate clicks from the UI show up as bugs instead of silent no-ops.

View Source
var ErrNextcloudAccountMissing = errors.New("no nextcloud account configured for this instance")

ErrNextcloudAccountMissing means the single nextcloud account was removed out-of-band between the migration and this cleanup call.

View Source
var ErrNextcloudUnreachable = errors.New("nextcloud unreachable")

ErrNextcloudUnreachable wraps any error surfaced while probing the Nextcloud server other than an explicit auth rejection (401/403). The caller should translate it to a 502 Bad Gateway for the HTTP client.

Functions

func CancelMigration

func CancelMigration(
	ctx context.Context,
	inst *instance.Instance,
	migrationID string,
	rmq rabbitmq.Service,
) error

CancelMigration validates the request and publishes a cancel command. It intentionally does NOT mutate the tracking document: the terminal state transition is owned by the migration service so there is a single writer for it.

The diagnostic logger is pulled from ctx via logger.FromContext so the caller can attach its request-scoped fields (migration_id, etc.) once with logger.WithContext rather than threading an extra parameter.

Error contract, in priority order, so callers can map via errors.Is:

  • ErrMigrationNotFound: no tracking doc with this id on the instance.
  • ErrMigrationAlreadyTerminal: the migration has already reached a terminal state (completed, failed, or canceled).
  • ErrMigrationBrokerUnavailable: the RabbitMQ publish failed. Unlike trigger, the tracking doc is NOT marked failed: a cancel publish failure does not invalidate a migration that is already running. The user retries, or the migration finishes normally.
  • any other error: treat as an internal server failure.

func DeleteMigrationSource

func DeleteMigrationSource(
	ctx context.Context,
	inst *instance.Instance,
	migrationID string,
) error

DeleteMigrationSource removes the Nextcloud content ingested by the given migration, then empties the user's trashbin so the migrated bytes stop counting against quota.

SourcePath == "/" enumerates the WebDAV home and deletes each top-level child: Nextcloud refuses DELETE on the home itself with 403, so a single call would fail. An empty SourcePath is treated as "/" to match legacy docs predating source_path persistence.

Error contract (priority order, for errors.Is mapping):

func EnsureAccount

func EnsureAccount(inst *instance.Instance, ncURL, login, password, userID string) (string, error)

EnsureAccount upserts the single Nextcloud account for the instance and returns its id. If an account already exists, its auth block and webdav_user_id are rewritten with the given values regardless of what they were before — this is the keyed-by-type policy that keeps the Settings UI free of orphans at the cost of multi-account support. The password is encrypted at rest before persistence.

func FetchUserIDWithCredentials

func FetchUserIDWithCredentials(ctx context.Context, nextcloudURL, username, password string) (string, error)

FetchUserIDWithCredentials probes the OCS cloud/user endpoint and returns the user ID, or webdav.ErrInvalidAuth if the credentials are rejected. The logger used for diagnostics is pulled from ctx via logger.FromContext, so callers should attach a request-scoped logger with logger.WithContext before calling.

https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html

func FindNextcloudAccount

func FindNextcloudAccount(inst *instance.Instance) (*couchdb.JSONDoc, error)

FindNextcloudAccount returns the unique Nextcloud account for the given instance, or (nil, nil) if none exists yet. By design there is at most one account with `account_type: "nextcloud"` per instance: the migration trigger endpoint overwrites the existing account on every call so a retry with a corrected password or a different login does not leave orphaned docs the Settings UI cannot surface.

If multiple legacy nextcloud accounts exist (left over from when the konnector flow kept one account per (url, login) pair), the function returns the first one scanned. Newly-triggered migrations will update that single doc; the rest stay in the database untouched until a real cleanup is wired up. A linear scan is cheaper than maintaining a Mango index because the per-instance account count is tiny.

Types

type File

type File struct {
	DocID       string `json:"id,omitempty"`
	Type        string `json:"type"`
	Name        string `json:"name"`
	Path        string `json:"path"`
	Size        uint64 `json:"size,omitempty"`
	Mime        string `json:"mime,omitempty"`
	Class       string `json:"class,omitempty"`
	UpdatedAt   string `json:"updated_at,omitempty"`
	ETag        string `json:"etag,omitempty"`
	RestorePath string `json:"restore_path,omitempty"`
	// contains filtered or unexported fields
}

func (*File) Clone

func (f *File) Clone() couchdb.Doc

func (*File) DocType

func (f *File) DocType() string

func (*File) ID

func (f *File) ID() string

func (*File) Included

func (f *File) Included() []jsonapi.Object
func (f *File) Links() *jsonapi.LinksList

func (*File) Relationships

func (f *File) Relationships() jsonapi.RelationshipMap

func (*File) Rev

func (f *File) Rev() string

func (*File) SetID

func (f *File) SetID(id string)

func (*File) SetRev

func (f *File) SetRev(id string)

type Migration

type Migration struct {
	DocID           string            `json:"_id,omitempty"`
	DocRev          string            `json:"_rev,omitempty"`
	Status          string            `json:"status"`
	TargetDir       string            `json:"target_dir"`
	SourcePath      string            `json:"source_path,omitempty"`
	Progress        MigrationProgress `json:"progress"`
	Errors          []MigrationError  `json:"errors"`
	Skipped         []SkippedFile     `json:"skipped"`
	StartedAt       *time.Time        `json:"started_at"`
	FinishedAt      *time.Time        `json:"finished_at"`
	CancelRequested bool              `json:"cancel_requested,omitempty"`
	CanceledAt      *time.Time        `json:"canceled_at,omitempty"`
	SourceDeletedAt *time.Time        `json:"source_deleted_at,omitempty"`
}

Migration is the io.cozy.nextcloud.migrations tracking document.

The schema (especially the nested Progress object) is the contract with twake-migration-nextcloud. Flat counters would crash the service's progress reducer because it spreads doc.progress and adds to its fields.

CancelRequested and CanceledAt are written by the migration service; the Stack round-trips them without modification. SourcePath and SourceDeletedAt are written only by the Stack (trigger and delete-source respectively); SourceDeletedAt is never cleared once stamped.

func FindActiveMigration

func FindActiveMigration(inst *instance.Instance) (*Migration, error)

FindActiveMigration returns the first pending or running migration, or (nil, nil) if none. A missing doctype database or index is treated as "no active migration" so the first call on a fresh instance succeeds.

func NewPendingMigration

func NewPendingMigration(targetDir, sourcePath string) *Migration

NewPendingMigration returns a fresh Migration document in the pending state. Errors and Skipped are explicit empty slices so the JSON serialization produces "[]" rather than "null": the migration service consumes them as arrays and would crash on null. sourcePath must be the normalized Nextcloud-side path from the trigger request.

func TriggerMigration

func TriggerMigration(
	ctx context.Context,
	inst *instance.Instance,
	req TriggerMigrationRequest,
	rmq rabbitmq.Service,
	log logger.Logger,
) (*Migration, error)

TriggerMigration is the single entry point for "start a Nextcloud migration for this instance with these credentials". It probes the remote host, serializes itself against concurrent triggers via a per-instance lock, upserts the io.cozy.accounts document, creates a pending tracking document, and publishes the migration command to RabbitMQ. On success it returns the created tracking document.

Error contract, in priority order, so callers can map them to HTTP status codes via errors.Is:

func (*Migration) Clone

func (m *Migration) Clone() couchdb.Doc

func (*Migration) DocType

func (m *Migration) DocType() string

func (*Migration) ID

func (m *Migration) ID() string

func (*Migration) Included

func (m *Migration) Included() []jsonapi.Object

func (*Migration) IsTerminal

func (m *Migration) IsTerminal() bool

IsTerminal reports whether the migration has reached a state that the Stack must not try to cancel (completed, failed, or canceled).

func (m *Migration) Links() *jsonapi.LinksList

func (*Migration) MarkFailed

func (m *Migration) MarkFailed(inst *instance.Instance, cause error) error

func (*Migration) Relationships

func (m *Migration) Relationships() jsonapi.RelationshipMap

func (*Migration) Rev

func (m *Migration) Rev() string

func (*Migration) SetID

func (m *Migration) SetID(id string)

func (*Migration) SetRev

func (m *Migration) SetRev(rev string)

type MigrationError

type MigrationError struct {
	Path    string    `json:"path"`
	Message string    `json:"message"`
	At      time.Time `json:"at"`
}

type MigrationProgress

type MigrationProgress struct {
	FilesImported int64 `json:"files_imported"`
	FilesTotal    int64 `json:"files_total"`
	BytesImported int64 `json:"bytes_imported"`
	BytesTotal    int64 `json:"bytes_total"`
}

type NextCloud

type NextCloud struct {
	// contains filtered or unexported fields
}

func New

func New(inst *instance.Instance, accountID string) (*NextCloud, error)

func (*NextCloud) Copy

func (nc *NextCloud) Copy(oldPath, newPath string) error

func (*NextCloud) Delete

func (nc *NextCloud) Delete(path string) error

func (*NextCloud) DeleteTrash

func (nc *NextCloud) DeleteTrash(path string) error

func (*NextCloud) Download

func (nc *NextCloud) Download(path string) (*webdav.Download, error)

func (*NextCloud) Downstream

func (nc *NextCloud) Downstream(path, dirID string, kind OperationKind, cozyMetadata *vfs.FilesCozyMetadata, failOnConflict bool) (*vfs.FileDoc, error)

func (*NextCloud) EmptyTrash

func (nc *NextCloud) EmptyTrash() error

func (*NextCloud) ListFiles

func (nc *NextCloud) ListFiles(path string) ([]jsonapi.Object, error)

func (*NextCloud) ListTrashed

func (nc *NextCloud) ListTrashed(path string) ([]jsonapi.Object, error)

func (*NextCloud) Mkdir

func (nc *NextCloud) Mkdir(path string) error

func (*NextCloud) Move

func (nc *NextCloud) Move(oldPath, newPath string) error

func (*NextCloud) Restore

func (nc *NextCloud) Restore(path string) error

func (*NextCloud) Size

func (nc *NextCloud) Size(path string) (uint64, error)

Size returns the recursive byte total of the resource at path, as reported by Nextcloud's cached `oc:size` property. Works on the account root (pass an empty string or "/") and on any sub-folder. Equivalent to a single Depth:0 PROPFIND on the server, not a tree walk, so the cost is constant regardless of how many files the folder contains.

func (*NextCloud) Upload

func (nc *NextCloud) Upload(path, mime string, contentLength int64, body io.Reader) error

func (*NextCloud) Upstream

func (nc *NextCloud) Upstream(path, from string, kind OperationKind) error

type OCSPayload

type OCSPayload struct {
	OCS struct {
		Data struct {
			UserID string `json:"id"`
		} `json:"data"`
	} `json:"ocs"`
}

type OperationKind

type OperationKind int
const (
	MoveOperation OperationKind = iota
	CopyOperation
)

type SkippedFile

type SkippedFile struct {
	Path   string `json:"path"`
	Reason string `json:"reason"`
	Size   int64  `json:"size"`
}

type TriggerMigrationRequest

type TriggerMigrationRequest struct {
	NextcloudURL         string
	NextcloudLogin       string
	NextcloudAppPassword string
	SourcePath           string
	// TargetDir is the absolute Cozy path under which the migration service
	// writes imported files. Empty means "use DefaultMigrationTargetDir".
	TargetDir string
}

TriggerMigrationRequest carries the user-supplied inputs needed to start a bulk Nextcloud-to-Cozy migration. Field semantics match the HTTP request body on POST /remote/nextcloud/migration.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL