filesystem

package
v0.0.0-...-3ab43b7 Latest Latest
Warning

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

Go to latest
Published: Nov 14, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

Documentation

Index

Constants

View Source
const (
	RootInode uint64 = 1

	DefaultChunkSize = 4 * bytesPerMiB
)

Variables

View Source
var (
	ErrOperationRecordCorrupt = errors.New("filesystem: corrupt operation record")
	ErrOperationIDReused      = errors.New("filesystem: operation ID reused with different intent")
	ErrOperationRetired       = errors.New("filesystem: operation identity retired")
	ErrOperationPending       = errors.New("filesystem: operation outcome pending")
	ErrOperationGeneration    = errors.New("filesystem: operation generation changed")
)
View Source
var (
	ErrOperationSessionNotFound = errors.New("filesystem: operation session not found")
	ErrOperationSessionClosed   = errors.New("filesystem: operation session closed")
	ErrOperationQuota           = errors.New("filesystem: operation journal quota exceeded")
)
View Source
var (
	ErrStoreRequired       = errors.New("filesystem: store is required")
	ErrDispatcherRequired  = errors.New("filesystem: dispatcher is required")
	ErrNotFound            = errors.New("filesystem: no such file or directory")
	ErrExists              = errors.New("filesystem: file exists")
	ErrNotDir              = errors.New("filesystem: not a directory")
	ErrIsDir               = errors.New("filesystem: is a directory")
	ErrNotEmpty            = errors.New("filesystem: directory not empty")
	ErrCrossDevice         = errors.New("filesystem: cross-domain rename")
	ErrInvalid             = errors.New("filesystem: invalid argument")
	ErrUnsupported         = errors.New("filesystem: operation not supported")
	ErrStaleHome           = errors.New("filesystem: stale or migrating file home")
	ErrContentionLimit     = errors.New("filesystem: contention retry limit reached")
	ErrMoveJobNotFound     = errors.New("filesystem: move job not found")
	ErrMoveFenceLost       = errors.New("filesystem: move job lost its epoch fence")
	ErrPlacementRequired   = errors.New("filesystem: placement resolver is required")
	ErrInodeCollisionLimit = errors.New("filesystem: inode allocation collision limit reached")
)
View Source
var (
	ErrWritePlanningFailed = errors.New("filesystem: write planning failed")
)

Functions

func IsWriteConflict

func IsWriteConflict(err error) bool

IsWriteConflict reports a definitive OCC rejection. The leader proxy reconstructs store.ErrWriteConflict from a typed forwarded Aborted status. Textual gRPC errors cannot certify that an attempt did not commit.

Types

type AttemptOutcome

type AttemptOutcome uint8

AttemptOutcome is backed by the replicated operation row. In particular, absence of a row or an RPC timeout never means Aborted.

const (
	AttemptUnknown AttemptOutcome = iota
	AttemptCommitted
	AttemptAborted
)

type CreateOptions

type CreateOptions struct {
	Mode     uint32
	UID      uint32
	GID      uint32
	ClientID []byte
}

type CreateResult

type CreateResult struct {
	Inode uint64
	FH    uint64
	Stat  Stat
}

type DirEntry

type DirEntry struct {
	Inode uint64   `json:"inode"`
	Type  FileType `json:"type"`
}

type Dirent

type Dirent struct {
	Name  []byte
	Inode uint64
	Type  FileType
}

type Dispatcher

type Dispatcher interface {
	Dispatch(context.Context, *kv.OperationGroup[kv.OP]) (*kv.CoordinateResponse, error)
}

type FSUsage

type FSUsage struct {
	Files uint64 `json:"files"`
	Bytes uint64 `json:"bytes"`
}

type FileType

type FileType string
const (
	TypeFile      FileType = "file"
	TypeDirectory FileType = "directory"
)

type Home

type Home struct {
	HomeSlot       uint64    `json:"home_slot"`
	TargetHomeSlot uint64    `json:"target_home_slot,omitempty"`
	State          HomeState `json:"state"`
	Epoch          uint64    `json:"epoch"`
}

type HomeState

type HomeState string
const (
	HomeStateActive    HomeState = "active"
	HomeStateMigrating HomeState = "migrating"
)

type InodeMeta

type InodeMeta struct {
	Inode      uint64   `json:"inode"`
	Parent     uint64   `json:"parent"`
	Type       FileType `json:"type"`
	Mode       uint32   `json:"mode"`
	UID        uint32   `json:"uid"`
	GID        uint32   `json:"gid"`
	Size       uint64   `json:"size"`
	AtimeNsec  int64    `json:"atime_nsec"`
	MtimeNsec  int64    `json:"mtime_nsec"`
	CtimeNsec  int64    `json:"ctime_nsec"`
	ChunkSize  uint64   `json:"chunk_size"`
	HomeSlot   uint64   `json:"home_slot"`
	Epoch      uint64   `json:"epoch"`
	Nlink      uint32   `json:"nlink"`
	Generation uint64   `json:"generation"`
	Orphaned   bool     `json:"orphaned,omitempty"`
}

func (InodeMeta) Stat

func (m InodeMeta) Stat() Stat

type IntentKind

type IntentKind string
const (
	IntentKindCreate IntentKind = "create"
	IntentKindDelete IntentKind = "delete"
	IntentKindMove   IntentKind = "move"
)

type IntentState

type IntentState struct {
	ID        []byte     `json:"id"`
	Kind      IntentKind `json:"kind"`
	Phase     string     `json:"phase"`
	Inode     uint64     `json:"inode,omitempty"`
	Parent    uint64     `json:"parent,omitempty"`
	Name      []byte     `json:"name,omitempty"`
	JobID     []byte     `json:"job_id,omitempty"`
	CreatedAt int64      `json:"created_at_nsec"`
	UpdatedAt int64      `json:"updated_at_nsec"`
}

type LeaseReapStats

type LeaseReapStats struct {
	ExpiredRefs        uint64
	OrphanedInodesGCed uint64
}

type MoveJob

type MoveJob struct {
	ID             []byte    `json:"id"`
	Inode          uint64    `json:"inode"`
	SourceHome     uint64    `json:"source_home"`
	TargetHome     uint64    `json:"target_home"`
	TargetGroup    uint64    `json:"target_group"`
	MigrationEpoch uint64    `json:"migration_epoch"`
	SwitchEpoch    uint64    `json:"switch_epoch,omitempty"`
	Phase          MovePhase `json:"phase"`
	Cursor         []byte    `json:"cursor,omitempty"`
	CopiedChunks   uint64    `json:"copied_chunks,omitempty"`
	CleanedChunks  uint64    `json:"cleaned_chunks,omitempty"`
	CreatedAt      int64     `json:"created_at_nsec"`
	UpdatedAt      int64     `json:"updated_at_nsec"`
}

type MovePhase

type MovePhase string
const (
	MovePhaseTargetCleanup MovePhase = "target_cleanup"
	MovePhaseCopy          MovePhase = "copy"
	MovePhaseSwitch        MovePhase = "switch"
	MovePhaseSourceCleanup MovePhase = "source_cleanup"
	MovePhaseCompleted     MovePhase = "completed"
)

type OpenHandleLease

type OpenHandleLease struct {
	Inode       uint64 `json:"inode"`
	ClientID    []byte `json:"client_id"`
	FH          uint64 `json:"fh"`
	CreatedNsec int64  `json:"created_nsec"`
	ExpiresNsec int64  `json:"expires_nsec,omitempty"`
}

type OperationID

type OperationID struct {
	Filesystem [16]byte `json:"filesystem"`
	Session    [16]byte `json:"session"`
	Sequence   uint64   `json:"sequence"`
}

type OperationJournal

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

OperationJournal stores one logical request and its result in the same transaction as its effects. FUSE semantic planners will call this module in M3; it never infers an abort from a missing result after an uncertain RPC.

func NewOperationJournal

func NewOperationJournal(service *Service) (*OperationJournal, error)

func (*OperationJournal) Acknowledge

func (j *OperationJournal) Acknowledge(ctx context.Context, id OperationID) error

Acknowledge retires a terminal identity only after the caller has attempted its reply. The session retains a reject-before watermark and bounded gaps, so deleting the detailed row cannot allow a delayed duplicate to execute.

func (*OperationJournal) Admit

func (j *OperationJournal) Admit(ctx context.Context, id OperationID, intent []byte, resultLimit, effectLimit uint32) (operationRecord, error)

Admit records the immutable intent before an effect can be proposed. A duplicate identity returns its original record only for identical bytes.

func (*OperationJournal) AdvanceGeneration

func (j *OperationJournal) AdvanceGeneration(ctx context.Context, id OperationID, old [16]byte) (operationRecord, error)

AdvanceGeneration fences an old attempt by committing a new version of its transaction primary. A delayed old publication either committed that primary first (making this CAS fail) or must fail its read-set validation against the new version. Legacy rows without the primary guarantee remain unresolved.

func (*OperationJournal) BeginAttempt

func (j *OperationJournal) BeginAttempt(ctx context.Context, id OperationID) (operationRecord, error)

BeginAttempt records a fresh transaction generation before any effect is dispatched. A duplicate caller receives the same active attempt identity.

func (*OperationJournal) Cancel

func (j *OperationJournal) Cancel(ctx context.Context, id OperationID) (operationRecord, error)

Cancel must win an OCC transaction against any active publication. A lost cancellation response remains unknown until Resolve observes a terminal row.

func (*OperationJournal) CancelWithResult

func (j *OperationJournal) CancelWithResult(ctx context.Context, id OperationID, result []byte) (operationRecord, error)

CancelWithResult retains a typed failure when planning ended before any publication was dispatched. The result is immutable once cancellation wins.

func (*OperationJournal) CloseSession

func (j *OperationJournal) CloseSession(ctx context.Context, id OperationSessionID) error

CloseSession is a durable fence. Pending attempts must reach a terminal state first, and the closed row stays as a non-expiring incarnation tombstone.

func (*OperationJournal) EnsureFilesystem

func (j *OperationJournal) EnsureFilesystem(ctx context.Context) ([16]byte, error)

EnsureFilesystem returns the single Raft-replicated identity shared by all mounts. A conflict rereads the winner; a lost response remains unknown.

func (*OperationJournal) FenceSession

func (j *OperationJournal) FenceSession(ctx context.Context, id OperationSessionID) error

FenceSession stops a crashed incarnation from publishing any new result. Pending operations remain durable and must be resolved/cancelled before a replacement session for the same owner can be registered.

func (*OperationJournal) PlanSnapshot

func (j *OperationJournal) PlanSnapshot(ctx context.Context, id OperationID, attempt [16]byte) (uint64, error)

PlanSnapshot establishes the OCC timestamp before a planner reads data. Every predicate key read at this timestamp must be included in ReadKeys.

func (*OperationJournal) PlanSnapshotAtRoute

func (j *OperationJournal) PlanSnapshotAtRoute(ctx context.Context, id OperationID, attempt [16]byte) (operationPlanningSnapshot, error)

PlanSnapshotAtRoute pins the placement used by the planner. A sharded planner must carry RouteVersion into publication or semantic rejection.

func (*OperationJournal) Publish

func (j *OperationJournal) Publish(ctx context.Context, id OperationID, attempt [16]byte, plan operationAttemptPlan) (operationRecord, error)

Publish commits the result record and every effect under the recorded generation. The session key is also a mutation, so closing the session and an in-flight attempt cannot both succeed on the same snapshot.

func (*OperationJournal) RecoverClosedSession

func (j *OperationJournal) RecoverClosedSession(ctx context.Context, id OperationSessionID, limit int) (int, error)

RecoverClosedSession cancels pending operations after their session fence wins. A conflicting prepared transaction leaves its outcome unknown; this method stops and must be resumed after the transaction resolver progresses.

func (*OperationJournal) RecoverOwnerSession

func (j *OperationJournal) RecoverOwnerSession(ctx context.Context, filesystemID [16]byte, owner string) error

RecoverOwnerSession fences a previous mount incarnation and resolves its pending attempts before the owner registers a replacement session.

func (*OperationJournal) RegisterSession

func (j *OperationJournal) RegisterSession(ctx context.Context, filesystemID [16]byte, owner string) (OperationSessionID, error)

func (*OperationJournal) RegisterSessionWithID

func (j *OperationJournal) RegisterSessionWithID(ctx context.Context, id OperationSessionID, owner string) error

RegisterSessionWithID lets a mount retain its chosen incarnation across a lost registration reply. It never substitutes a new identity on retry.

func (*OperationJournal) Reject

func (j *OperationJournal) Reject(ctx context.Context, id OperationID, attempt [16]byte, readTS uint64, intent []byte, result []byte, readKeys [][]byte) (operationRecord, error)

Reject publishes a terminal semantic failure only after the caller has validated its predicates on the same snapshot. OCC conflicts remain pending attempts and must not be stored as semantic rejection.

func (*OperationJournal) RejectAtRoute

func (j *OperationJournal) RejectAtRoute(ctx context.Context, id OperationID, attempt [16]byte, readTS, routeVersion uint64, intent []byte, result []byte, readKeys [][]byte) (operationRecord, error)

func (*OperationJournal) Resolve

func (j *OperationJournal) Resolve(ctx context.Context, id OperationID) (operationRecord, error)

Resolve is a leader-fenced read. A missing result is still pending, and a committed result is returned only when every effect's version is visible.

func (*OperationJournal) ResolveAttempt

func (j *OperationJournal) ResolveAttempt(ctx context.Context, id OperationID, attempt [16]byte) (AttemptOutcome, operationRecord, error)

func (*OperationJournal) RetireUnadmitted

func (j *OperationJournal) RetireUnadmitted(ctx context.Context, id OperationID) error

RetireUnadmitted fences an identity whose reply was attempted before an operation record was admitted. The session and record read set makes this race atomically with admission: an in-flight admission either wins and must be resolved normally, or observes the retired identity and cannot publish.

type OperationJournalStats

type OperationJournalStats struct {
	Sessions       uint64
	ClosedSessions uint64
	Pending        uint64
	RetainedCount  uint64
	RetainedBytes  uint64
	MaxBytes       uint64
}

OperationJournalStats is a snapshot of retained strict-mode work across every mount session. Pending and retained bytes must drain before rollback.

type OperationSessionID

type OperationSessionID struct {
	Filesystem [16]byte
	Session    [16]byte
}

type OperationState

type OperationState uint8
const (
	OperationPending OperationState = iota + 1
	OperationCommitted
	OperationRejected
	OperationCancelled
)

type OperationalObserver

type OperationalObserver interface {
	ObserveChunkRead(time.Duration)
	ObserveChunkWrite(time.Duration)
	ObserveHomeEpochConflict()
	ObserveInodeIDCollisionRetry()
	ObserveMoveJob(jobID []byte, active bool)
	ObservePlacementStats(PlacementStats)
}

type Option

type Option func(*Service)

func WithCapacity

func WithCapacity(capacity uint64) Option

func WithChunkSize

func WithChunkSize(size uint64) Option

func WithClock

func WithClock(now func() time.Time) Option

func WithHomeSlotAllocator

func WithHomeSlotAllocator(homeSlot func(uint64) uint64) Option

func WithIDAllocator

func WithIDAllocator(alloc func() (uint64, error)) Option

func WithMaxFiles

func WithMaxFiles(maxFiles uint64) Option

func WithOpenHandleLeaseTTL

func WithOpenHandleLeaseTTL(ttl time.Duration) Option

func WithOperationalObserver

func WithOperationalObserver(observer OperationalObserver) Option

func WithReadObserver

func WithReadObserver(observer kv.Coordinator) Option

WithReadObserver enables bounded, leader-local KeyViz observations for public FUSE reads and mutation probes before transaction dispatch.

type PlacementStats

type PlacementStats struct {
	FilesByGroup     map[uint64]uint64
	MultiShardFiles  uint64
	MultiShardInodes []uint64
	MoveInflight     uint64
	OpenHandleLeases uint64
	OrphanedInodes   uint64
}

type ReaddirResult

type ReaddirResult struct {
	Entries    []Dirent
	NextCookie string
}

type RecoveryStats

type RecoveryStats struct {
	MoveJobsResumed uint64
	MoveJobsCleared uint64
	IntentsCleared  uint64
}

type Service

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

func NewService

func NewService(st store.MVCCStore, dispatch Dispatcher, opts ...Option) (*Service, error)

func (*Service) AcknowledgeOperation

func (s *Service) AcknowledgeOperation(ctx context.Context, id OperationID) error

AcknowledgeOperation retires a terminal operation after its FUSE reply was attempted and its participant proofs were verified by the adapter.

func (*Service) CloseOperationSession

func (s *Service) CloseOperationSession(ctx context.Context, id OperationSessionID) error

func (*Service) Create

func (s *Service) Create(ctx context.Context, parent uint64, name []byte, opts CreateOptions) (CreateResult, error)

func (*Service) CreateWithOperation

func (s *Service) CreateWithOperation(ctx context.Context, id OperationID, parent uint64, name []byte, opts CreateOptions) (CreateResult, error)

CreateWithOperation retains the created inode, open handle and original Stat.

func (*Service) Flush

func (s *Service) Flush(context.Context, uint64, uint64) error

func (*Service) Fsync

func (s *Service) Fsync(ctx context.Context, inode uint64, _ uint64, _ bool) error

func (*Service) GetAttr

func (s *Service) GetAttr(ctx context.Context, inode uint64) (Stat, error)

func (*Service) GetFileHome

func (s *Service) GetFileHome(ctx context.Context, inode uint64) (Home, error)

GetFileHome returns the durable placement state for an inode.

func (*Service) InitializeRoot

func (s *Service) InitializeRoot(ctx context.Context, mode uint32, uid uint32, gid uint32) error

func (*Service) ListFilePlacementStats

func (s *Service) ListFilePlacementStats(ctx context.Context) (PlacementStats, error)

ListFilePlacementStats computes operator-facing placement state at one fenced snapshot and publishes it to the configured observer.

func (*Service) ListOperationJournalStats

func (s *Service) ListOperationJournalStats(ctx context.Context) (OperationJournalStats, error)

func (*Service) Mkdir

func (s *Service) Mkdir(ctx context.Context, parent uint64, name []byte, opts CreateOptions) (CreateResult, error)

func (*Service) MkdirWithOperation

func (s *Service) MkdirWithOperation(ctx context.Context, id OperationID, parent uint64, name []byte, opts CreateOptions) (CreateResult, error)

MkdirWithOperation retains the created directory inode and original Stat.

func (*Service) MoveFile

func (s *Service) MoveFile(ctx context.Context, inode uint64, targetGroup uint64) (MoveJob, error)

MoveFile synchronously executes a resumable whole-file migration. The job is durable before any chunk copy starts, so RecoverIntents can resume it after a process or leader restart.

func (*Service) Open

func (s *Service) Open(ctx context.Context, inode uint64, clientID []byte) (uint64, error)

func (*Service) OpenHandleLeaseTTL

func (s *Service) OpenHandleLeaseTTL() time.Duration

OpenHandleLeaseTTL exposes the configured renewal budget to FUSE.

func (*Service) OpenWithOperation

func (s *Service) OpenWithOperation(ctx context.Context, id OperationID, inode uint64, clientID []byte) (uint64, error)

OpenWithOperation retains the original handle and its lease publication.

func (*Service) Read

func (s *Service) Read(ctx context.Context, inode uint64, _ uint64, offset uint64, size uint64) ([]byte, error)

func (*Service) Readdir

func (s *Service) Readdir(ctx context.Context, inode uint64, cookie string, limit int) (ReaddirResult, error)

func (*Service) ReapExpiredOpenHandleLeases

func (s *Service) ReapExpiredOpenHandleLeases(ctx context.Context, limit int) (LeaseReapStats, error)

func (*Service) RecoverIntents

func (s *Service) RecoverIntents(ctx context.Context, limit int) (RecoveryStats, error)

RecoverIntents resumes durable move jobs and aborts prepared namespace intents. Create/delete publish their intent before the atomic namespace txn and delete it inside that txn, so a surviving marker proves the namespace mutation did not commit and is safe to clear.

func (*Service) RefreshOpenHandleLease

func (s *Service) RefreshOpenHandleLease(ctx context.Context, inode uint64, fh uint64, clientID []byte) error

func (*Service) RefreshOpenHandleLeaseWithOperation

func (s *Service) RefreshOpenHandleLeaseWithOperation(ctx context.Context, id OperationID, inode, fh uint64, clientID []byte) error

RefreshOpenHandleLeaseWithOperation cannot recreate a released or reaped lease.

func (*Service) Release

func (s *Service) Release(ctx context.Context, inode uint64, fh uint64, clientID []byte) error

func (*Service) ReleaseWithOperation

func (s *Service) ReleaseWithOperation(ctx context.Context, id OperationID, inode, fh uint64, clientID []byte) error

ReleaseWithOperation makes a handle release terminal under one operation ID.

func (*Service) Rename

func (s *Service) Rename(ctx context.Context, oldParent uint64, oldName []byte, newParent uint64, newName []byte) error

func (*Service) RenameWithOperation

func (s *Service) RenameWithOperation(ctx context.Context, id OperationID, oldParent uint64, oldName []byte, newParent uint64, newName []byte) error

RenameWithOperation journals one same-directory rename and its result.

func (*Service) Resolve

func (s *Service) Resolve(ctx context.Context, parent uint64, name []byte) (uint64, error)

func (*Service) ResumeMoveFile

func (s *Service) ResumeMoveFile(ctx context.Context, jobID []byte) (MoveJob, error)

ResumeMoveFile advances a durable migration state machine until completion.

func (*Service) Rmdir

func (s *Service) Rmdir(ctx context.Context, parent uint64, name []byte) error

func (*Service) RmdirWithOperation

func (s *Service) RmdirWithOperation(ctx context.Context, id OperationID, parent uint64, name []byte) error

RmdirWithOperation journals an empty-directory removal and its result.

func (*Service) SetAttr

func (s *Service) SetAttr(ctx context.Context, inode uint64, mask SetAttrMask, attrs SetAttr) (Stat, error)

func (*Service) SetAttrWithOperation

func (s *Service) SetAttrWithOperation(ctx context.Context, id OperationID, inode uint64, mask SetAttrMask, attrs SetAttr) (Stat, error)

SetAttrWithOperation journals attribute changes and retains the original Stat.

func (*Service) StatFS

func (s *Service) StatFS(ctx context.Context, _ uint64) (StatFS, error)

func (*Service) Truncate

func (s *Service) Truncate(ctx context.Context, inode uint64, size uint64) error
func (s *Service) Unlink(ctx context.Context, parent uint64, name []byte) error

func (*Service) UnlinkWithOperation

func (s *Service) UnlinkWithOperation(ctx context.Context, id OperationID, parent uint64, name []byte) error

UnlinkWithOperation records one file removal and its result under id. Directory removal has a separate emptiness predicate and planner.

func (*Service) ValidateOpenHandleLease

func (s *Service) ValidateOpenHandleLease(ctx context.Context, inode uint64, fh uint64, clientID []byte) error

ValidateOpenHandleLease checks a handle without extending its lease.

func (*Service) VerifyOperationReceipt

func (s *Service) VerifyOperationReceipt(ctx context.Context, id OperationID) error

VerifyOperationReceipt reads the authoritative terminal record and checks every participant proof before a local FUSE barrier can discharge it.

func (*Service) Write

func (s *Service) Write(ctx context.Context, inode uint64, _ uint64, offset uint64, data []byte) (int, error)

func (*Service) WriteWithOperation

func (s *Service) WriteWithOperation(ctx context.Context, id OperationID, inode, offset uint64, data []byte) (int, error)

WriteWithOperation publishes one fixed-offset write and its original byte count under id. A pending duplicate is resolved without issuing a new write. The caller owns acknowledgement after it has attempted the FUSE reply.

type SetAttr

type SetAttr struct {
	Mode      uint32
	UID       uint32
	GID       uint32
	Size      uint64
	AtimeNsec int64
	MtimeNsec int64
}

type SetAttrMask

type SetAttrMask struct {
	Mode  bool
	UID   bool
	GID   bool
	Size  bool
	Atime bool
	Mtime bool
}

type Stat

type Stat struct {
	Inode      uint64
	Generation uint64
	Type       FileType
	Mode       uint32
	UID        uint32
	GID        uint32
	Size       uint64
	Nlink      uint32
	AtimeNsec  int64
	MtimeNsec  int64
	CtimeNsec  int64
}

type StatFS

type StatFS struct {
	ChunkSize uint64
	Files     uint64
	FreeFiles uint64
	Capacity  uint64
	Free      uint64
}

Directories

Path Synopsis
Package fstest provides in-memory test doubles for exercising the filesystem service without a Raft cluster.
Package fstest provides in-memory test doubles for exercising the filesystem service without a Raft cluster.

Jump to

Keyboard shortcuts

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