client

package
v1.4.8 Latest Latest
Warning

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

Go to latest
Published: Mar 7, 2024 License: BSD-3-Clause Imports: 25 Imported by: 1

Documentation

Index

Constants

View Source
const (
	// APIVersionV2Upload supports extended image upload functionality.
	APIVersionV2Upload = "2.0.0-alpha.1"
	// APIVersionV2ArchTags supports extended arch tags functionality.
	APIVersionV2ArchTags = "2.0.0-alpha.2"
)
View Source
const (

	// OptionS3Compliant indicates a 100% S3 compatible object store is being used by backend library server
	OptionS3Compliant = "s3compliant"
)
View Source
const Scheme = "library"

Scheme is the required scheme for Library URIs.

Variables

View Source
var (

	// ErrUnauthorized represents HTTP status "401 Unauthorized"
	ErrUnauthorized = errors.New("unauthorized")

	// ErrRefSchemeNotValid represents a ref with an invalid scheme.
	ErrRefSchemeNotValid = errors.New("library: ref scheme not valid")
	// ErrRefUserNotPermitted represents a ref with an invalid user.
	ErrRefUserNotPermitted = errors.New("library: user not permitted in ref")
	// ErrRefQueryNotPermitted represents a ref with an invalid query.
	ErrRefQueryNotPermitted = errors.New("library: query not permitted in ref")
	// ErrRefFragmentNotPermitted represents a ref with an invalid fragment.
	ErrRefFragmentNotPermitted = errors.New("library: fragment not permitted in ref")
	// ErrRefPathNotValid represents a ref with an invalid path.
	ErrRefPathNotValid = errors.New("library: ref path not valid")
	// ErrRefTagsNotValid represents a ref with invalid tags.
	ErrRefTagsNotValid = errors.New("library: ref tags not valid")
	// ErrNotFound is returned by when a resource is not found (http status 404)
	ErrNotFound = errors.New("not found")
)
View Source
var DefaultConfig = &Config{}

DefaultConfig is a configuration that uses default values.

View Source
var LibraryModels = []string{"Entity", "Collection", "Container", "Image", "Blob"}

LibraryModels lists names of valid models in the database

Functions

func IDInSlice

func IDInSlice(a string, list []string) bool

IDInSlice returns true if ID is present in the slice

func ImageHash

func ImageHash(filePath string) (result string, err error)

ImageHash returns the appropriate hash for a provided image file

e.g. sif.<uuid> or sha256.<sha256>

func IsImageHash

func IsImageHash(refPart string) bool

IsImageHash returns true if the provided string is valid as a unique hash for an image

func IsLibraryPullRef

func IsLibraryPullRef(libraryRef string) bool

IsLibraryPullRef returns true if the provided string is a valid library reference for a pull operation.

func IsLibraryPushRef

func IsLibraryPushRef(libraryRef string) bool

IsLibraryPushRef returns true if the provided string is a valid library reference for a push operation.

func IsRefPart

func IsRefPart(refPart string) bool

IsRefPart returns true if the provided string is valid as a component of a library URI (i.e. a valid entity, collection etc. name)

func ParseLibraryPath added in v0.1.0

func ParseLibraryPath(libraryRef string) (entity string, collection string, container string, tags []string)

func PrettyPrint

func PrettyPrint(v interface{})

PrettyPrint - Debug helper, print nice json for any interface

func SliceWithoutID

func SliceWithoutID(list []string, a string) []string

SliceWithoutID returns slice with specified ID removed

func StringInSlice

func StringInSlice(a string, list []string) bool

StringInSlice returns true if string is present in the slice

Types

type AbortMultipartUploadRequest added in v0.5.0

type AbortMultipartUploadRequest struct {
	UploadID string `json:"uploadID"`
}

AbortMultipartUploadRequest is sent to abort V2 multipart image upload

type ArchImageTag added in v0.4.0

type ArchImageTag struct {
	Arch    string
	Tag     string
	ImageID string
}

ArchImageTag - A simple mapping from a architecture and tag string to bson ID. Not stored in the DB but used by API calls setting tags

type ArchTagMap added in v0.4.0

type ArchTagMap map[string]TagMap

ArchTagMap is a mapping of a string architecture to a TagMap, and hence to Images.

e.g. {
			"amd64":    { "latest": 507f1f77bcf86cd799439011 },
			"ppc64le":  { "latest": 507f1f77bcf86cd799439012 },
		}

type ArchTagsResponse added in v0.4.0

type ArchTagsResponse struct {
	Data  ArchTagMap      `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

ArchTagsResponse - Response from the API for a v2 tags request (with arch)

type BaseModel

type BaseModel struct {
	ModelManager `json:",omitempty"`
	Deleted      bool      `json:"deleted"`
	CreatedBy    string    `json:"createdBy"`
	CreatedAt    time.Time `json:"createdAt"`
	UpdatedBy    string    `json:"updatedBy,omitempty"`
	UpdatedAt    time.Time `json:"updatedAt,omitempty"`
	DeletedBy    string    `json:"deletedBy,omitempty"`
	DeletedAt    time.Time `json:"deletedAt,omitempty"`
	Owner        string    `json:"owner,omitempty"`
}

BaseModel - has an ID, soft deletion marker, and Audit struct

func (BaseModel) GetCreated

func (m BaseModel) GetCreated() (auditUser string, auditTime time.Time)

GetCreated - Convenience method to get creation stamps if working with an interface

func (BaseModel) GetDeleted

func (m BaseModel) GetDeleted() (auditUser string, auditTime time.Time)

GetDeleted - Convenience method to get deletino stamps if working with an interface

func (BaseModel) GetUpdated

func (m BaseModel) GetUpdated() (auditUser string, auditTime time.Time)

GetUpdated - Convenience method to get update stamps if working with an interface

func (BaseModel) IsDeleted

func (m BaseModel) IsDeleted() bool

IsDeleted - Convenience method to check soft deletion state if working with an interface

type Blob

type Blob struct {
	BaseModel
	ID          string `json:"id"`
	Bucket      string `json:"bucket"`
	Key         string `json:"key"`
	Size        int64  `json:"size"`
	ContentHash string `json:"contentHash"`
	Status      string `json:"status"`
}

Blob - Binary data object (e.g. container image file) stored in a Backend Uses object store bucket/key semantics

func (Blob) GetID

func (b Blob) GetID() string

GetID - Convenience method to get model ID if working with an interface

type Client

type Client struct {
	// Base URL of the service.
	BaseURL *url.URL
	// Auth token to include in the Authorization header of each request (if supplied).
	AuthToken string
	// User agent to include in each request (if supplied).
	UserAgent string
	// HTTPClient to use to make HTTP requests.
	HTTPClient *http.Client
	// Logger to be used when output is generated
	Logger log.Logger
}

Client describes the client details.

func NewClient

func NewClient(cfg *Config) (*Client, error)

NewClient sets up a new Cloud-Library Service client with the specified base URL and auth token.

func (*Client) ConcurrentDownloadImage added in v1.2.0

func (c *Client) ConcurrentDownloadImage(ctx context.Context, dst *os.File, arch, path, tag string, spec *Downloader, pb ProgressBar) error

ConcurrentDownloadImage implements a multi-part (concurrent) downloader for Cloud Library images. spec is used to define transfer parameters. pb is an optional progress bar interface. If pb is nil, NoopProgressBar is used.

The downloader will handle source files of all sizes and is not limited to only files larger than Downloader.PartSize. It will automatically adjust the concurrency for source files that do not meet minimum size for multi-part downloads.

func (*Client) DeleteImage added in v0.4.3

func (c *Client) DeleteImage(ctx context.Context, imageRef, arch string) error

DeleteImage deletes requested imageRef.

func (*Client) DownloadImage added in v0.1.0

func (c *Client) DownloadImage(ctx context.Context, w io.Writer, arch, path, tag string, callback func(int64, io.Reader, io.Writer) error) error

DownloadImage will retrieve an image from the Container Library, saving it into the specified io.Writer. The timeout value for this operation is set within the context. It is recommended to use a large value (ie. 1800 seconds) to prevent timeout when downloading large images.

func (*Client) GetImage added in v0.1.0

func (c *Client) GetImage(ctx context.Context, arch string, imageRef string) (*Image, error)

GetImage returns the Image object if exists; returns ErrNotFound if image is not found, otherwise error.

func (*Client) GetVersion

func (c *Client) GetVersion(ctx context.Context) (vi VersionInfo, err error)

GetVersion gets version information from the Cloud-Library Service. The context controls the lifetime of the request.

func (*Client) Search added in v0.1.0

func (c *Client) Search(ctx context.Context, args map[string]string) (*SearchResults, error)

Search performs a library search, returning any matching collections, containers, entities, or images.

args specifies key-value pairs to be used as a search spec, such as "arch" (ie. "amd64") or "signed" (valid values "true" or "false").

"value" is a required keyword for all searches. It will be matched against all collections (Entity, Collection, Container, and Image)

Multiple architectures may be searched by specifying a comma-separated list (ie. "amd64,arm64") for the value of "arch".

Match all collections with name "thename":

c.Search(ctx, map[string]string{"value": "thename"})

Match all images with name "imagename" and arch "amd64"

c.Search(ctx, map[string]string{
    "value": "imagename",
    "arch": "amd64"
})

Note: if 'arch' and/or 'signed' are specified, the search is limited in scope only to the "Image" collection.

func (*Client) UploadImage added in v0.1.0

func (c *Client) UploadImage(ctx context.Context, r io.ReadSeeker, path, arch string, tags []string, description string, callback UploadCallback) (*UploadImageComplete, error)

UploadImage will push a specified image from an io.ReadSeeker up to the Container Library, The timeout value for this operation is set within the context. It is recommended to use a large value (ie. 1800 seconds) to prevent timeout when uploading large images.

type Collection

type Collection struct {
	BaseModel
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Entity      string   `json:"entity"`
	Containers  []string `json:"containers"`
	Size        int64    `json:"size"`
	Private     bool     `json:"private"`
	// CustomData can hold a user-provided string for integration purposes
	// not used by the library itself.
	CustomData string `json:"customData"`
	// Computed fields that will not be stored - JSON response use only
	EntityName string `json:"entityName,omitempty"`
}

Collection - Second level in the library, holds a collection of containers

func (Collection) GetID

func (c Collection) GetID() string

GetID - Convenience method to get model ID if working with an interface

func (Collection) LibraryURI

func (c Collection) LibraryURI() string

LibraryURI - library:// URI to the collection

type CollectionResponse

type CollectionResponse struct {
	Data  Collection      `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

CollectionResponse - Response from the API for an Collection request

type CompleteMultipartUploadRequest added in v0.5.0

type CompleteMultipartUploadRequest struct {
	UploadID       string          `json:"uploadID"`
	CompletedParts []CompletedPart `json:"completedParts"`
}

CompleteMultipartUploadRequest is sent to complete V2 multipart image upload

type CompleteMultipartUploadResponse added in v0.5.0

type CompleteMultipartUploadResponse struct {
	Data  UploadImageComplete `json:"data"`
	Error *jsonresp.Error     `json:"error,omitempty"`
}

CompleteMultipartUploadResponse - Response from the API for a multipart image upload complete request

type CompletedPart added in v0.5.0

type CompletedPart struct {
	PartNumber int    `json:"partNumber"`
	Token      string `json:"token"`
}

CompletedPart represents a single part of a multipart image upload

type Config

type Config struct {
	// Base URL of the service.
	BaseURL string
	// Auth token to include in the Authorization header of each request (if supplied).
	AuthToken string
	// User agent to include in each request (if supplied).
	UserAgent string
	// HTTPClient to use to make HTTP requests (if supplied).
	HTTPClient *http.Client
	// Logger to be used when output is generated
	Logger log.Logger
}

Config contains the client configuration.

type Container

type Container struct {
	BaseModel
	ID              string   `json:"id"`
	Name            string   `json:"name"`
	Description     string   `json:"description"`
	FullDescription string   `json:"fullDescription"`
	Collection      string   `json:"collection"`
	Images          []string `json:"images"`
	// This base TagMap without architecture support is for old clients only
	// (Singularity <=3.3) to preserve non-architecture-aware behavior
	ImageTags TagMap `json:"imageTags"`
	// We now have a 2 level map for new clients, keeping tags per architecture
	ArchTags      ArchTagMap `json:"archTags"`
	Size          int64      `json:"size"`
	DownloadCount int64      `json:"downloadCount"`
	Stars         int        `json:"stars"`
	Private       bool       `json:"private"`
	ReadOnly      bool       `json:"readOnly"`
	// CustomData can hold a user-provided string for integration purposes
	// not used by the library itself.
	CustomData string `json:"customData"`
	// Computed fields that will not be stored - JSON response use only
	Entity         string `json:"entity,omitempty"`
	EntityName     string `json:"entityName,omitempty"`
	CollectionName string `json:"collectionName,omitempty"`
}

Container - Third level of library. Inside a collection, holds images for a particular container

func (Container) GetID

func (c Container) GetID() string

GetID - Convenience method to get model ID if working with an interface

func (Container) LibraryURI

func (c Container) LibraryURI() string

LibraryURI - library:// URI to the container

func (Container) TagList

func (c Container) TagList() string

TagList - return a sorted space delimited list of tags

type ContainerResponse

type ContainerResponse struct {
	Data  Container       `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

ContainerResponse - Response from the API for an Container request

type Downloader added in v1.2.0

type Downloader struct {
	// Concurrency defines concurrency for multi-part downloads.
	Concurrency uint

	// PartSize specifies size of part for multi-part downloads. Default is 5 MiB.
	PartSize int64

	// BufferSize specifies buffer size used for multi-part downloader routine.
	// Default is 32 KiB.
	// Deprecated: this value will be ignored. It is retained for backwards compatibility.
	BufferSize int64
}

Downloader defines concurrency (# of requests) and part size for download operation.

type Entity

type Entity struct {
	BaseModel
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Collections []string `json:"collections"`
	Size        int64    `json:"size"`
	Quota       int64    `json:"quota"`
	// DefaultPrivate set true will make any new Collections in this entity
	// private at the time of creation.
	DefaultPrivate bool `json:"defaultPrivate"`
	// CustomData can hold a user-provided string for integration purposes
	// not used by the library itself.
	CustomData string `json:"customData"`
}

Entity - Top level entry in the library, contains collections of images for a user or group

func (Entity) GetID

func (e Entity) GetID() string

GetID - Convenience method to get model ID if working with an interface

func (Entity) LibraryURI

func (e Entity) LibraryURI() string

LibraryURI - library:// URI to the entity

type EntityResponse

type EntityResponse struct {
	Data  Entity          `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

EntityResponse - Response from the API for an Entity request

type Image

type Image struct {
	BaseModel
	ID           string   `json:"id"`
	Hash         string   `json:"hash"`
	Description  string   `json:"description"`
	Container    string   `json:"container"`
	Blob         string   `json:"blob,omitempty"`
	Size         int64    `json:"size"`
	Uploaded     bool     `json:"uploaded"`
	Signed       *bool    `json:"signed,omitempty"`
	Architecture *string  `json:"arch,omitempty"`
	Fingerprints []string `json:"fingerprints,omitempty"`
	Encrypted    *bool    `json:"encrypted,omitempty"`
	// CustomData can hold a user-provided string for integration purposes
	// not used by the library itself.
	CustomData string `json:"customData"`
	// Computed fields that will not be stored - JSON response use only
	Entity               string   `json:"entity,omitempty"`
	EntityName           string   `json:"entityName,omitempty"`
	Collection           string   `json:"collection,omitempty"`
	CollectionName       string   `json:"collectionName,omitempty"`
	ContainerName        string   `json:"containerName,omitempty"`
	Tags                 []string `json:"tags,omitempty"`
	ContainerDescription string   `json:"containerDescription,omitempty"`
	ContainerStars       int      `json:"containerStars"`
	ContainerDownloads   int64    `json:"containerDownloads"`
}

Image - Represents a Singularity image held by the library for a particular Container

func (Image) GetID

func (img Image) GetID() string

GetID - Convenience method to get model ID if working with an interface

type ImageResponse

type ImageResponse struct {
	Data  Image           `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

ImageResponse - Response from the API for an Image request

type ImageTag

type ImageTag struct {
	Tag     string
	ImageID string
}

ImageTag - A single mapping from a string to bson ID. Not stored in the DB but used by API calls setting tags

type ModelManager

type ModelManager interface {
	GetID() string
}

ModelManager - Generic interface for models which must have a bson ObjectID

type MultipartUpload added in v0.5.0

type MultipartUpload struct {
	UploadID   string            `json:"uploadID"`
	TotalParts int               `json:"totalParts"`
	PartSize   int64             `json:"partSize"`
	Options    map[string]string `json:"options"`
}

MultipartUpload - Contains data for multipart image upload start request

type MultipartUploadStartRequest added in v0.5.0

type MultipartUploadStartRequest struct {
	Size int64 `json:"filesize"`
}

MultipartUploadStartRequest is sent to initiate V2 multipart image upload

type MultipartUploadStartResponse added in v0.5.0

type MultipartUploadStartResponse struct {
	Data  MultipartUpload `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

MultipartUploadStartResponse - Response from the API for a multipart image upload start request

type NoopProgressBar added in v1.2.0

type NoopProgressBar struct{}

NoopProgressBar implements ProgressBarInterface to allow disabling the progress bar

func (*NoopProgressBar) Abort added in v1.2.0

func (*NoopProgressBar) Abort(bool)

Abort is a no-op

func (*NoopProgressBar) IncrBy added in v1.2.0

func (*NoopProgressBar) IncrBy(int)

IncrBy is a no-op

func (*NoopProgressBar) Init added in v1.2.0

func (*NoopProgressBar) Init(int64)

Init is a no-op

func (*NoopProgressBar) ProxyReader added in v1.2.0

func (*NoopProgressBar) ProxyReader(r io.Reader) io.ReadCloser

ProxyReader is a no-op

func (*NoopProgressBar) Wait added in v1.2.0

func (*NoopProgressBar) Wait()

Wait is a no-op

type ProgressBar added in v1.2.0

type ProgressBar interface {
	// Initialize progress bar. Argument is size of file to set progress bar limit.
	Init(int64)

	// ProxyReader wraps r with metrics required for progress tracking. Only useful for
	// single stream downloads.
	ProxyReader(io.Reader) io.ReadCloser

	// IncrBy increments the progress bar. It is called after each concurrent
	// buffer transfer.
	IncrBy(int)

	// Abort terminates the progress bar.
	Abort(bool)

	// Wait waits for the progress bar to complete.
	Wait()
}

ProgressBar provides a minimal interface for interacting with a progress bar. Init is called prior to concurrent download operation.

type QuotaResponse added in v0.5.8

type QuotaResponse struct {
	QuotaTotalBytes int64 `json:"quotaTotal"`
	QuotaUsageBytes int64 `json:"quotaUsage"`
}

QuotaResponse contains quota usage and total available storage

type Ref added in v0.1.0

type Ref struct {
	Host string   // host or host:port
	Path string   // project or entity/project
	Tags []string // list of tags
}

A Ref represents a parsed Library URI.

The general form represented is:

scheme:[//host][/]path[:tags]

The host contains both the hostname and port, if present. These values can be accessed using the Hostname and Port methods.

Examples of valid URIs:

library:path:tags
library:/path:tags
library:///path:tags
library://host/path:tags
library://host:port/path:tags

The tags component is a comma-separated list of one or more tags.

func Parse added in v0.1.0

func Parse(rawRef string) (*Ref, error)

Parse parses a raw Library reference.

func ParseAmbiguous added in v1.3.0

func ParseAmbiguous(rawRef string) (*Ref, error)

ParseAmbiguous behaves like Parse, but takes into account ambiguity that exists within Singularity Library references that begin with the prefix "library://".

In particular, Singularity supports hostless Library references in the form of "library://path". This creates ambiguity in whether or not a host is present in the path or not. To account for this, ParseAmbiguous treats library references beginning with "library://" followed by one or three path components (ex. "library://a", "library://a/b/c") as hostless. All other references are treated the same as Parse.

func (*Ref) Hostname added in v0.1.0

func (r *Ref) Hostname() string

Hostname returns r.Host, without any port number.

If Host is an IPv6 literal with a port number, Hostname returns the IPv6 literal without the square brackets. IPv6 literals may include a zone identifier.

func (*Ref) Port added in v0.1.0

func (r *Ref) Port() string

Port returns the port part of u.Host, without the leading colon. If u.Host doesn't contain a port, Port returns an empty string.

func (*Ref) String added in v0.1.0

func (r *Ref) String() string

String reassembles the ref into a valid URI string. The general form of the result is one of:

scheme:path[:tags]
scheme://host/path[:tags]

If u.Host is empty, String uses the first form; otherwise it uses the second form.

type SearchResponse

type SearchResponse struct {
	Data  SearchResults   `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

SearchResponse - Response from the API for a search request

type SearchResults

type SearchResults struct {
	Entities    []Entity     `json:"entity"`
	Collections []Collection `json:"collection"`
	Containers  []Container  `json:"container"`
	Images      []Image      `json:"image"`
}

SearchResults - Results structure for searches

type TagMap

type TagMap map[string]string

TagMap is a mapping of a string tag, to an ObjectID that refers to an Image e.g. { "latest": 507f1f77bcf86cd799439011 }

type TagsResponse

type TagsResponse struct {
	Data  TagMap          `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

TagsResponse - Response from the API for a tags request

type UploadCallback

type UploadCallback interface {
	// Initializes the callback given a file size and source file Reader
	InitUpload(int64, io.Reader)
	// (optionally) can return a proxied Reader
	GetReader() io.Reader
	// TerminateUpload is called if the upload operation is interrupted before completion
	Terminate()
	// called when the upload operation is complete
	Finish()
}

UploadCallback defines an interface used to perform a call-out to set up the source file Reader.

type UploadImage

type UploadImage struct {
	UploadURL string `json:"uploadURL"`
}

UploadImage - Contains requisite data for direct S3 image upload support

type UploadImageComplete added in v0.5.8

type UploadImageComplete struct {
	Quota        QuotaResponse `json:"quota"`
	ContainerURL string        `json:"containerUrl"`
}

UploadImageComplete contains data from upload image completion

type UploadImageCompleteRequest added in v0.2.0

type UploadImageCompleteRequest struct{}

UploadImageCompleteRequest is sent to complete V2 image upload; it is currently unused.

type UploadImageCompleteResponse added in v0.5.8

type UploadImageCompleteResponse struct {
	Data  UploadImageComplete `json:"data"`
	Error *jsonresp.Error     `json:"error,omitempty"`
}

UploadImageCompleteResponse is the response to the upload image completion request

type UploadImagePart added in v0.5.0

type UploadImagePart struct {
	PresignedURL string `json:"presignedURL"`
}

UploadImagePart - Contains data for multipart image upload part request

type UploadImagePartRequest added in v0.5.0

type UploadImagePartRequest struct {
	PartSize       int64  `json:"partSize"`
	UploadID       string `json:"uploadID"`
	PartNumber     int    `json:"partNumber"`
	SHA256Checksum string `json:"sha256sum"`
}

UploadImagePartRequest is sent prior to each part in a multipart upload

type UploadImagePartResponse added in v0.5.0

type UploadImagePartResponse struct {
	Data  UploadImagePart `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

UploadImagePartResponse - Response from the API for a multipart image upload part request

type UploadImageRequest added in v0.2.0

type UploadImageRequest struct {
	Size           int64  `json:"filesize"`
	MD5Checksum    string `json:"md5sum,omitempty"`
	SHA256Checksum string `json:"sha256sum,omitempty"`
}

UploadImageRequest is sent to initiate V2 image upload

type UploadImageResponse added in v0.2.0

type UploadImageResponse struct {
	Data  UploadImage     `json:"data"`
	Error *jsonresp.Error `json:"error,omitempty"`
}

UploadImageResponse - Response from the API for an image upload request

type VersionInfo

type VersionInfo struct {
	Version    string `json:"version"`
	APIVersion string `json:"apiVersion"`
}

VersionInfo contains version information.

Jump to

Keyboard shortcuts

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