storage

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Apr 14, 2024 License: AGPL-3.0 Imports: 20 Imported by: 0

Documentation

Index

Constants

View Source
const UserIdInternal = -1

Variables

This section is empty.

Functions

func CreateDocumentDir

func CreateDocumentDir(documentId string) error

CreateDocumentDir creates (if not yet existing) directory for document.

func CreatePreviewDir

func CreatePreviewDir(documentId string) error

CreatePreviewDir creates (if not yet existing) directory for preview.

func DocumentPath

func DocumentPath(documentId string) string

DocumentPath returns path for document by its id. Function splits documents to 2-level directories inside config.C.Processing.DocumentsDir. Id must be at least 3 characters long, else empty string is returned.

func MoveFile

func MoveFile(from string, to string) error

MoveFile moves file from old location to new. It copies file, if necessary.

func NewTx added in v0.6.0

func NewTx(db *Database, ctx context.Context) (*tx, error)

func PreviewPath

func PreviewPath(documentId string) string

DocumentPath returns path for document preview by its id. Function splits previews to 2-level directories inside config.C.Processing.PreviewsDir. Id must be at least 3 characters long, else empty string is returned.

func TempFilePath

func TempFilePath(documentId string) string

TempFilePath returns filename in temporary directory for given id.

Types

type AuthStore

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

func (*AuthStore) DeleteExpiredAuthTokens

func (s *AuthStore) DeleteExpiredAuthTokens() (int, error)

func (*AuthStore) FlushCache

func (s *AuthStore) FlushCache()

func (*AuthStore) GetToken

func (s *AuthStore) GetToken(key string, updateLastSeen bool) (*models.Token, error)

func (*AuthStore) InsertToken

func (s *AuthStore) InsertToken(token *models.Token) error

func (*AuthStore) Name

func (s *AuthStore) Name() string

func (*AuthStore) RevokeToken

func (s *AuthStore) RevokeToken(key string) error

func (*AuthStore) UpdateTokenConfirmation

func (s *AuthStore) UpdateTokenConfirmation(key string, confirmed time.Time) error

type Database

type Database struct {
	UserStore     *UserStore
	DocumentStore *DocumentStore
	JobStore      *JobStore
	MetadataStore *MetadataStore
	StatsStore    *StatsStore
	RuleStore     *RuleStore
	AuthStore     *AuthStore
	// contains filtered or unexported fields
}

Database connects to postgresql database and contains store for each model/relation.

func NewDatabase

func NewDatabase(conf config.Database) (*Database, error)

NewDatabase returns working instance of database connection.

func NewMockDatabase

func NewMockDatabase(matcher sqlmock.QueryMatcher) (*Database, sqlmock.Sqlmock, error)

NewMockDatabase returns mock database instance

func (*Database) Close

func (d *Database) Close() error

func (*Database) Engine

func (d *Database) Engine() *sqlx.DB

func (*Database) Exec added in v0.6.0

func (d *Database) Exec(query string, args ...interface{}) (sql.Result, error)

func (*Database) ExecContext added in v0.6.0

func (d *Database) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)

func (*Database) ExecContextSq added in v0.6.0

func (d *Database) ExecContextSq(ctx context.Context, sql squirrel.Sqlizer) (sql.Result, error)

func (*Database) ExecSq added in v0.6.0

func (d *Database) ExecSq(sql squirrel.Sqlizer) (sql.Result, error)

func (*Database) Get added in v0.6.0

func (d *Database) Get(destination interface{}, query string, args ...interface{}) error

func (*Database) GetContextSq added in v0.6.0

func (d *Database) GetContextSq(ctx context.Context, destination interface{}, sql squirrel.Sqlizer) error

func (*Database) GetSq added in v0.6.0

func (d *Database) GetSq(destination interface{}, sql squirrel.Sqlizer) error

func (*Database) QueryContextSq added in v0.6.0

func (d *Database) QueryContextSq(ctx context.Context, sql squirrel.Sqlizer) (*sqlx.Rows, error)

func (*Database) QuerySq added in v0.6.0

func (d *Database) QuerySq(sql squirrel.Sqlizer) (*sqlx.Rows, error)

func (*Database) Select added in v0.6.0

func (d *Database) Select(destination interface{}, sql string, args ...interface{}) error

func (*Database) SelectContextSq added in v0.6.0

func (d *Database) SelectContextSq(ctx context.Context, destination interface{}, sql squirrel.Sqlizer) error

func (*Database) SelectSq added in v0.6.0

func (d *Database) SelectSq(destination interface{}, sql squirrel.Sqlizer) error

type DocumentStore

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

func NewDocumentStore

func NewDocumentStore(db *sqlx.DB, mt *MetadataStore) *DocumentStore

func (*DocumentStore) AddVisited

func (s *DocumentStore) AddVisited(userId int, documentId string) error

func (*DocumentStore) BulkUpdateDocuments added in v0.5.0

func (s *DocumentStore) BulkUpdateDocuments(exec SqlExecer, userId int, docs []string, lang models.Lang, date time.Time) error

func (*DocumentStore) Create

func (s *DocumentStore) Create(exec SqlExecer, doc *models.Document) error

func (*DocumentStore) DeleteDocument

func (s *DocumentStore) DeleteDocument(docId string) error

func (*DocumentStore) GetByHash

func (s *DocumentStore) GetByHash(userId int, hash string) (*models.Document, error)

GetByHash returns a document by its hash and user.

func (*DocumentStore) GetContent

func (s *DocumentStore) GetContent(id string) (*string, error)

GetContent returns full content. If userId != 0, user must own the document of given id.

func (*DocumentStore) GetDocument

func (s *DocumentStore) GetDocument(execer SqlExecer, id string) (*models.Document, error)

GetDocument returns document by its id.

func (*DocumentStore) GetDocumentHistory

func (s *DocumentStore) GetDocumentHistory(userId int, docId string) (*[]models.DocumentHistory, error)

func (*DocumentStore) GetDocuments

func (s *DocumentStore) GetDocuments(exec SqlExecer, userId int, paging Paging, sort SortKey, limitContent bool, showTrash bool, showSharesDocs bool) (*[]models.Document, int, error)

GetDocuments returns user's documents according to paging. In addition, return total count of documents available.

func (*DocumentStore) GetDocumentsById added in v0.5.0

func (s *DocumentStore) GetDocumentsById(exec SqlExecer, userId int, id []string) (*[]models.Document, error)

GetDocument returns document by its id. If userId != 0, user must be owner of the document.

func (*DocumentStore) GetDocumentsInTrashbin

func (s *DocumentStore) GetDocumentsInTrashbin(deletedAt time.Time) ([]string, error)

func (*DocumentStore) GetPermissions added in v0.6.0

func (s *DocumentStore) GetPermissions(exec SqlExecer, documentId string, userId int) (owner bool, perm models.Permissions, err error)

func (*DocumentStore) GetSharedUsers added in v0.6.0

func (s *DocumentStore) GetSharedUsers(exec SqlExecer, docId string) (*[]models.DocumentSharePermission, error)

func (*DocumentStore) MarkDocumentDeleted

func (s *DocumentStore) MarkDocumentDeleted(exec SqlExecer, userId int, docId string) error

func (*DocumentStore) MarkDocumentNonDeleted

func (s *DocumentStore) MarkDocumentNonDeleted(exec SqlExecer, userId int, docId string) error

func (DocumentStore) Name

func (s DocumentStore) Name() string

func (*DocumentStore) SetDocumentContent

func (s *DocumentStore) SetDocumentContent(id string, content string) error

SetDocumentContent sets content for given document id

func (*DocumentStore) SetModifiedAt

func (s *DocumentStore) SetModifiedAt(exec SqlExecer, docIds []string, modifiedAt time.Time) error

func (*DocumentStore) Update

func (s *DocumentStore) Update(exec SqlExecer, userId int, doc *models.Document) error

Update sets complete document record, not just changed attributes. Thus document must be read before updating. Metadata history is not saved.

func (*DocumentStore) UpdateSharing added in v0.6.0

func (s *DocumentStore) UpdateSharing(exec SqlExecer, docId string, sharing *[]models.UpdateUserSharing) error

func (*DocumentStore) UserOwnsDocument

func (s *DocumentStore) UserOwnsDocument(documentId string, userId int) (bool, error)

UserOwnsDocumet returns true if user has ownership for document.

func (*DocumentStore) UserOwnsDocuments

func (s *DocumentStore) UserOwnsDocuments(queries SqlExecer, userId int, documents []string) (bool, error)

type Execer added in v0.6.0

type Execer interface {
	ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
	Exec(query string, args ...interface{}) (sql.Result, error)
}

type ExecerSq added in v0.6.0

type ExecerSq interface {
	ExecContextSq(ctx context.Context, sql squirrel.Sqlizer) (sql.Result, error)
	ExecSq(sql squirrel.Sqlizer) (sql.Result, error)
}

type JobStore

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

func (*JobStore) AddDocuments

func (s *JobStore) AddDocuments(exec SqlExecer, userId int, documents []string, steps []models.ProcessStep, trigger models.RuleTrigger) error

func (*JobStore) CancelDocumentProcessing

func (s *JobStore) CancelDocumentProcessing(documentId string) error

CancelDocumentProcessing removes all steps from processing queue for document.

func (*JobStore) CancelRunningProcesses

func (s *JobStore) CancelRunningProcesses() error

CancelRunningProcesses marks all processes that are currently running as not running.

func (*JobStore) CreateJob added in v0.5.0

func (s *JobStore) CreateJob(documentId string, job *models.Job) error

func (*JobStore) ForceProcessingByUser added in v0.5.0

func (s *JobStore) ForceProcessingByUser(userId int, steps []models.ProcessStep) error

func (*JobStore) ForceProcessingDocument added in v0.5.0

func (s *JobStore) ForceProcessingDocument(exec SqlExecer, documentId string, steps []models.ProcessStep) error

ForceProcessingDocument adds documents to process queue. If documentID != 0, mark only given document. If userId != 0, mark all documents for user. Else mark all documents for re-processing. FromStep is the first step and successive steps are expected to re-run as well.

func (*JobStore) GetDocumentStatus

func (s *JobStore) GetDocumentStatus(documentId string) (string, error)

GetDocumentStatus returns status for given document: pending, indexing, ready

func (*JobStore) GetDocumentsPendingProcessing

func (s *JobStore) GetDocumentsPendingProcessing() (*[]string, error)

GetDocumentsPendingProcessing returns list of document ids that are not currently being processed and have processing queued. Only first 50 documents are returned

func (*JobStore) GetJobsByDocumentId added in v0.5.0

func (s *JobStore) GetJobsByDocumentId(documentId string) (*[]models.Job, error)

GetJobsByDocumentId returns all jobs related to document

func (*JobStore) GetJobsByUserId added in v0.5.0

func (s *JobStore) GetJobsByUserId(userId int, paging Paging) (*[]models.JobComposite, error)

GetJobsByDocumentId returns all jobs related to document

func (*JobStore) GetNextStepForDocument added in v0.5.0

func (s *JobStore) GetNextStepForDocument(documentId string) (*models.ProcessItem, error)

GetNextStepForDocument returns next step that hasn't been started yet.

func (*JobStore) GetPendingProcessing

func (s *JobStore) GetPendingProcessing() (*[]models.ProcessItem, int, error)

GetPendingProcessing returns max 100 processQueue items ordered by created_at. Also returns total number of pending process_queues. Only returns steps for documents that are not currently being processed

func (*JobStore) IndexDocumentsByMetadata added in v0.5.0

func (s *JobStore) IndexDocumentsByMetadata(exec SqlExecer, userId int, keyId int, valueId int) error

IndexDocumentsByMetadata adds all documents that match the identifiers. If user != 0, use has to own the document, if keyId != 0, document has to have key, if valueId != 0, document has to have the value. Either key or value must be supplied.

func (*JobStore) MarkProcessingDone

func (s *JobStore) MarkProcessingDone(item *models.ProcessItem, ok bool) error

MarkProcessinDone update given item. If ok, remove record, else mark it as not running

func (JobStore) Name

func (s JobStore) Name() string

func (*JobStore) ProcessDocumentAllSteps added in v0.5.0

func (s *JobStore) ProcessDocumentAllSteps(documentId string, trigger models.RuleTrigger) error

ProcessDocumentAllSteps adds default processing steps for document. Document must be existing.

func (*JobStore) StartProcessItem

func (s *JobStore) StartProcessItem(item *models.ProcessItem, msg string) (*models.Job, error)

StartProcessItem attempts to mark processItem as running. If successful, create corresponding Job and return it.

func (*JobStore) UpdateJob added in v0.5.0

func (s *JobStore) UpdateJob(job *models.Job) error

type MetadataStore

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

func NewMetadataStore

func NewMetadataStore(db *sqlx.DB) *MetadataStore

func (*MetadataStore) CheckKeyValuesExist

func (s *MetadataStore) CheckKeyValuesExist(userId int, values []models.Metadata) error

CheckKeyValuesExist verifies key-value pairs exist and user owns them.

func (*MetadataStore) CreateKey

func (s *MetadataStore) CreateKey(userId int, key *models.MetadataKey) error

CreateKey creates new metadata key.

func (*MetadataStore) CreateTag

func (s *MetadataStore) CreateTag(userId int, tag *models.Tag) error

CreateTag creates new tag.

func (*MetadataStore) CreateValue

func (s *MetadataStore) CreateValue(value *models.MetadataValue) error

CreateValue creates new metadata value.

func (*MetadataStore) DeleteDocumentsMetadata

func (s *MetadataStore) DeleteDocumentsMetadata(exec SqlExecer, userId int, documents []string, metadata []models.Metadata) error

func (*MetadataStore) DeleteKey

func (s *MetadataStore) DeleteKey(exec SqlExecer, userId int, keyId int) error

DeleteKey deletes metadata key. If userId != 0, user has to own the key. This will cascade the deletion to any table that uses metadata keys too: document_metadata, rules.

func (*MetadataStore) DeleteValue

func (s *MetadataStore) DeleteValue(userId int, valueId int) error

DeleteValue deletes metadata value from key. If userId != 0, user has to own the value. This will cascade the deletion to any table that uses metadata keys too: document_metadata, rules.

func (*MetadataStore) GetDocumentMetadata

func (s *MetadataStore) GetDocumentMetadata(exec SqlExecer, userId int, documentId string) (*[]models.Metadata, error)

GetDocumentMetadata returns key-value metadata for given document. If userId != 0, user must own document.

func (*MetadataStore) GetDocumentTags

func (s *MetadataStore) GetDocumentTags(userId int, documentId string) (*[]models.Tag, error)

GetDocumentTags returns tags for given document.

func (*MetadataStore) GetKey

func (s *MetadataStore) GetKey(keyId int) (*models.MetadataKey, error)

func (*MetadataStore) GetKeys

func (s *MetadataStore) GetKeys(userId int, ids []int, sort SortKey, paging Paging) (*[]models.MetadataKeyAnnotated, int, error)

GetKeys returns all possible metadata-keys for user.

func (*MetadataStore) GetLinkedDocuments

func (s *MetadataStore) GetLinkedDocuments(userId int, docId string) ([]*models.LinkedDocument, error)

GetLinkedDocuments returns a list of documents that are linked to docId.

func (*MetadataStore) GetTag

func (s *MetadataStore) GetTag(userId, tagId int) (*models.TagComposite, error)

GetTag returns tag with given id.

func (*MetadataStore) GetTags

func (s *MetadataStore) GetTags(userid int, paging Paging) (*[]models.TagComposite, int, error)

GetTags returns all tags for user.

func (*MetadataStore) GetUserKeyValuesCached

func (s *MetadataStore) GetUserKeyValuesCached(userId int, key string) (*[]models.Metadata, error)

func (*MetadataStore) GetUserKeysCached

func (s *MetadataStore) GetUserKeysCached(userId int) (*[]models.MetadataKey, error)

func (*MetadataStore) GetUserLangsCached added in v0.5.0

func (s *MetadataStore) GetUserLangsCached(userId int) (*[]string, error)

func (*MetadataStore) GetUserValuesWithMatching

func (s *MetadataStore) GetUserValuesWithMatching(userId int) (*[]models.MetadataValue, error)

GetUserValuesWithMatching retusn all metadata values that have Metadatavalue.MatchDocuments enabled.

func (*MetadataStore) GetValues

func (s *MetadataStore) GetValues(keyId int, sort SortKey, paging Paging) (*[]models.MetadataValue, error)

GetValues returns all values to given key.

func (*MetadataStore) KeyValuePairExists

func (s *MetadataStore) KeyValuePairExists(userId, key, value int) (bool, error)

KeyValuePairExists checks whether given pair actually exists and is user owns them.

func (MetadataStore) Name

func (s MetadataStore) Name() string

func (*MetadataStore) Search added in v0.6.0

func (s *MetadataStore) Search(exec SqlExecer, userId int, query string) (*models.MetadataSearchResult, error)

func (*MetadataStore) UpdateDocumentKeyValues

func (s *MetadataStore) UpdateDocumentKeyValues(exec SqlExecer, userId int, documentId string, metadata []models.Metadata) error

UpdateDocumentKeyValues updates key-values for document.

func (*MetadataStore) UpdateKey

func (s *MetadataStore) UpdateKey(key *models.MetadataKey) error

func (*MetadataStore) UpdateLinkedDocuments

func (s *MetadataStore) UpdateLinkedDocuments(exec SqlExecer, userId int, docId string, docs []string) error

UpdateLinkedDocuments updates document. This does not validate ownership of the documents.

func (*MetadataStore) UpdateValue

func (s *MetadataStore) UpdateValue(value *models.MetadataValue) error

func (*MetadataStore) UpsertDocumentMetadata

func (s *MetadataStore) UpsertDocumentMetadata(exec SqlExecer, userId int, documents []string, metadata []models.Metadata) error

func (*MetadataStore) UserHasKey

func (s *MetadataStore) UserHasKey(userId, keyId int) (bool, error)

func (*MetadataStore) UserHasKeyValue

func (s *MetadataStore) UserHasKeyValue(userId, keyId, valueId int) (bool, error)

func (*MetadataStore) UserHasKeys

func (s *MetadataStore) UserHasKeys(exec SqlExecer, userId int, keys []int) (bool, error)

type Paging

type Paging struct {
	Offset int
	Limit  int
}

func (*Paging) Validate

func (p *Paging) Validate()

type PreferenceKey

type PreferenceKey string

type Querier added in v0.6.0

type Querier interface {
	QueryContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
	SelectContext(ctx context.Context, destination interface{}, query string, args ...interface{}) error
}

type QuerierSq added in v0.6.0

type QuerierSq interface {
	QueryContextSq(ctx context.Context, sql squirrel.Sqlizer) (*sqlx.Rows, error)
	QuerySq(sql squirrel.Sqlizer) (*sqlx.Rows, error)
	Select(destination interface{}, sql string, args ...interface{}) error
	SelectContextSq(ctx context.Context, destination interface{}, sql squirrel.Sqlizer) error
	SelectSq(destination interface{}, sql squirrel.Sqlizer) error
	GetContextSq(ctx context.Context, destination interface{}, sql squirrel.Sqlizer) error
	GetSq(destination interface{}, sql squirrel.Sqlizer) error
	Get(destination interface{}, query string, args ...interface{}) error
}

type Resource

type Resource interface {
	Name() string
	// contains filtered or unexported methods
}

Resource is a generic persistence storage for single resource type.

type RuleStore

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

RuleStore is storage for user-defined processing rules.

func (*RuleStore) AddRule

func (s *RuleStore) AddRule(execer SqlExecer, userId int, rule *models.Rule) error

func (*RuleStore) DeleteRule

func (s *RuleStore) DeleteRule(ruleId int) error

func (*RuleStore) GetActiveUserRules

func (s *RuleStore) GetActiveUserRules(userId int, trigger models.RuleTrigger) ([]*models.Rule, error)

GetActiveUresRules returns all enabled rules (with some limit) for given user.

func (*RuleStore) GetUserRule

func (s *RuleStore) GetUserRule(userId, ruleId int) (*models.Rule, error)

func (*RuleStore) GetUserRules

func (s *RuleStore) GetUserRules(exec SqlExecer, userId int, paging Paging, query, enabled string) ([]*models.Rule, int, error)

func (RuleStore) Name

func (r RuleStore) Name() string

func (*RuleStore) ReorderRules

func (s *RuleStore) ReorderRules(userId int, ids []int) error

func (*RuleStore) UpdateRule

func (s *RuleStore) UpdateRule(exec ExecerSq, userId int, rule *models.Rule) error

UpdateRule updates rule.

func (*RuleStore) UserOwnsRule

func (s *RuleStore) UserOwnsRule(userId, ruleId int) (bool, error)

type SortKey

type SortKey struct {
	Key             string
	Order           bool
	CaseInsensitive bool
}

SortKey contains sortable key and order. Order 'false' = ASC, 'true' = DESC.

func NewSortKey

func NewSortKey(key string, defaultKey string, order bool, caseInsensitive bool) SortKey

func (SortKey) QueryKey

func (s SortKey) QueryKey() string

func (*SortKey) SetDefaults

func (s *SortKey) SetDefaults(key string, order bool)

func (SortKey) SortOrder

func (s SortKey) SortOrder() string

func (*SortKey) Validate

func (s *SortKey) Validate(defaultKey string)

Validate validates sort keys and enforces the key to be legal.

type SqlExecer added in v0.6.0

type SqlExecer interface {
	Execer
	ExecerSq
	ExecerSq
	QuerierSq
}

type StatsStore

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

func NewStatsStore

func NewStatsStore(db *sqlx.DB) *StatsStore

func (*StatsStore) GetSystemStats

func (s *StatsStore) GetSystemStats() (*models.SystemStatistics, error)

func (*StatsStore) GetUserDocumentStats

func (s *StatsStore) GetUserDocumentStats(userId int) (*models.UserDocumentStatistics, error)

func (*StatsStore) Name

func (s *StatsStore) Name() string

type UserStore

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

func (*UserStore) AddPasswordResetToken

func (s *UserStore) AddPasswordResetToken(token *models.PasswordResetToken) error

func (*UserStore) AddUser

func (s *UserStore) AddUser(user *models.User) error

AddUser adds user. Id is updated.

func (*UserStore) DeleteExpiredPasswordResetTokens

func (s *UserStore) DeleteExpiredPasswordResetTokens() (int, error)

func (*UserStore) DeletePasswordResetToken

func (s *UserStore) DeletePasswordResetToken(tokenId int) error

func (*UserStore) FlushCache

func (s *UserStore) FlushCache()

func (*UserStore) GetPasswordResetTokenByHash

func (s *UserStore) GetPasswordResetTokenByHash(tokenId int) (*models.PasswordResetToken, error)

func (*UserStore) GetPreferenceValue

func (s *UserStore) GetPreferenceValue(userId int, key PreferenceKey) (string, error)

func (*UserStore) GetUser

func (s *UserStore) GetUser(userid int) (*models.User, error)

GetUser returns single user with id.

func (*UserStore) GetUserByEmail

func (s *UserStore) GetUserByEmail(email string) (*models.User, error)

func (*UserStore) GetUserByName

func (s *UserStore) GetUserByName(username string) (*models.User, error)

GetUserByName returns user matching username

func (*UserStore) GetUserPreferences

func (s *UserStore) GetUserPreferences(userid int) (*models.UserPreferences, error)

func (*UserStore) GetUsers

func (s *UserStore) GetUsers() (*[]models.User, error)

GetUsers returns all users.

func (*UserStore) GetUsersInfo

func (s *UserStore) GetUsersInfo() (*[]models.UserInfo, error)

func (*UserStore) Name

func (s *UserStore) Name() string

func (*UserStore) SetPreferenceValue

func (s *UserStore) SetPreferenceValue(userId int, key PreferenceKey, value string) error

func (*UserStore) TryLogin

func (s *UserStore) TryLogin(username, password string) (int, error)

TryLogin tries to log user in by password. Return userId = -1 and error if login fails.

func (*UserStore) Update

func (s *UserStore) Update(user *models.User) error

Update existing user. Username cannot be changed,

Directories

Path Synopsis
* Virtualpaper is a service to manage users paper documents in virtual format.
* Virtualpaper is a service to manage users paper documents in virtual format.

Jump to

Keyboard shortcuts

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