Skip to main content

网盘设计

如何设计实现网盘?

  • 网盘几个部分
    • 前端协议
      • 面向客户端
      • 提供统一接口
      • 有用户概念
      • 支持元信息
      • 最小单位为文件
    • 服务
      • 文件 <-> 对象 映射
      • 元信息维护
      • 权限控制
      • 协议适配
    • 后端存储
      • 解决数据实际存储问题
      • 一般为对象
      • 一般没有用户概念
      • 最小单位为 block、chunk、object
      • 一般支持多副本
  • 常见协议 S3、POSIX、WebDAV、SFTP、FTPS

考虑点

  • Storage as a Service
    • 要求后端能力强
  • Bring Your Own Storage
    • 要求能适配不同后端
  • Selfhosted
    • 要求能简单运维部署
  • 文件大小
    • 小文件
    • 图片
    • 大文件
    • 文档
  • 使用场景
    • POSIX - 完整的 fs 功能
    • 对象存储
    • 在线协同
    • 群件
      • ToB、共享、租户
    • 网盘
      • ToC、共享
    • 基于 Web 的文件管理器
      • 单机
  • WebDAV - CardDAV、CalDAV
    • natelindev/tsdav
      • WebDAV, CALDAV, and CARDDAV client for Nodejs and the Browser

实现方向

  • 围绕协议展开 - WebDAV
  • 围绕功能展开
    • 产品功能要求
    • 前端接口要求
    • 后端存储要求
  • 围绕服务展开
    • 现有服务提供的能力进行封装
  • Metadata+Chunk
    • 通常是底层存储的实现
    • 例如: seaweedfs, juicefs, garage
  • FS Meta+Object+Metadata
    • 在对象存储之上实现文件系统
    • FS Meta 可以存储在 DB
    • Object Key 为 sha
    • Metadata 记录关于 Object 的信息 - 便于不依赖 fs meta 直接访问操作
tip
  • murmur3,md5,sha1 用来做文件 Hash 是合适的,但不建议做唯一
  • 如果要唯一,可以使用 sha256

S3 API

  • Amazon S3 REST API Introduction
  • Actions
    • {Copy,Get,Delete,Put,Restore,List}Object
      • Acl
      • Attributes
      • LegalHold
      • LockConfiguration
      • Retention
      • Tagging
      • Torrent
    • {Create,Delete,Head,Get,List}Bucket
      • Acl
      • Policy
      • Cors
      • Encruption
      • Lifecycle
      • Replication
      • Tagging
      • Website
      • 少用
        • LifecycleConfiguration
        • Location
        • NotificationConfiguration
        • Logging
        • Versioning
        • RequestPayment
        • Notification
        • PolicyStatus
        • AnalyticsConfiguration
        • IntelligentTieringConfiguration
        • InventoryConfiguration
        • MetricsConfiguration
        • OwnershipControls
        • AccelerateConfiguration
    • {Abort,Complete,Create,List}MultipartUpload
    • GetPublicAccessBlock
    • ListParts
    • SelectObjectContent
    • UploadPart
    • UploadPartCopy
    • WriteGetObjectResponse
  • Amazon S3 Control Actions
    • {Create,Delete,Get,List}AccessPoint
    • {Create,Describe,List}Job
      • Tagging

实现方案分析

local

  • 简单的网盘就是将本地 fs 暴露到 网络
  • 直接使用本地文件系统非常灵活,但是还是需要做元数据的处理
  • 好处
    • 运维直观
    • 实现简单
    • 可直接使用现有的协议服务暴露存储
    • 单机存储容量可以非常高
    • 利用本地文件系统特性 - zfs
  • 坏处
    • 需要维护一致性 - 但如果只有一个入口相对容易
    • 不能水平扩容
    • 不容易处理加密
    • 副本、备份需要其他方案
    • 还是需要实现服务维护元数据
    • 元数据服务与协议服务分离存在一致性问题

seaweedfs

  • master+volume 提供 分布式对象存储
  • filer 实现 fs 接口
  • 支持暴露 s3 服务接口
  • 好处
    • 水平扩容
    • 副本、备份
  • 坏处
    • 运维
    • 依赖较多服务

mime

  • directory
    • inode/directory
      • mimetype, xdg
    • application/x-directory; charset=binary
      • file
  • Shared MIME-info Database
    • inode/blockdevice
    • inode/chardevice
    • inode/directory
    • inode/fifo
    • inode/mount-point
    • inode/socket
    • inode/symlink
# application/x-directory; charset=binary
file --mime-type --mime-encoding $PWD

layout

  • 面向个人
  • 面向企业
  • 参考
    • xdg base dir
    • linux fhs

meta

namespace.attribute
user.mime_type
trusted.md5sum,
system.posix_acl_access
security.selinux

file manager

thumb

filemeta

struct inode {
struct hlist_node i_hash; /* hash list */
struct list_head i_list; /* list of inodes */
struct list_head i_dentry; /* list of dentries */
unsigned long i_ino; /* inode number */
atomic_t i_count; /* reference counter */
umode_t i_mode; /* access permissions */
unsigned int i_nlink; /* number of hard links */
uid_t i_uid; /* user id of owner */
gid_t i_gid; /* group id of owner */
kdev_t i_rdev; /* real device node */
loff_t i_size; /* file size in bytes */
struct timespec i_atime; /* last access time */
struct timespec i_mtime; /* last modify time */
struct timespec i_ctime; /* last change time */
unsigned int i_blkbits; /* block size in bits */
unsigned long i_blksize; /* block size in bytes */
unsigned long i_version; /* version number */
unsigned long i_blocks; /* file size in blocks */
unsigned short i_bytes; /* bytes consumed */
spinlock_t i_lock; /* spinlock */
struct rw_semaphore i_alloc_sem; /* nests inside of i_sem */
struct semaphore i_sem; /* inode semaphore */
struct inode_operations *i_op; /* inode ops table */
struct file_operations *i_fop; /* default inode ops */
struct super_block *i_sb; /* associated superblock */
struct file_lock *i_flock; /* file lock list */
struct address_space *i_mapping; /* associated mapping */
struct address_space i_data; /* mapping for device */
struct dquot *i_dquot[MAXQUOTAS]; /* disk quotas for inode */
struct list_head i_devices; /* list of block devices */
struct pipe_inode_info *i_pipe; /* pipe information */
struct block_device *i_bdev; /* block device driver */
unsigned long i_dnotify_mask; /* directory notify mask */
struct dnotify_struct *i_dnotify; /* dnotify */
unsigned long i_state; /* state flags */
unsigned long dirtied_when; /* first dirtying time */
unsigned int i_flags; /* filesystem flags */
unsigned char i_sock; /* is this a socket? */
atomic_t i_writecount; /* count of writers */
void *i_security; /* security module */
__u32 i_generation; /* inode version number */
union {
void *generic_ip; /* filesystem-specific info */
} u;
};

struct inode_operations {
int (*create) (struct inode *, struct dentry *,int);
struct dentry * (*lookup) (struct inode *, struct dentry *);
int (*link) (struct dentry *, struct inode *, struct dentry *);
int (*unlink) (struct inode *, struct dentry *);
int (*symlink) (struct inode *, struct dentry *, const char *);
int (*mkdir) (struct inode *, struct dentry *, int);
int (*rmdir) (struct inode *, struct dentry *);
int (*mknod) (struct inode *, struct dentry *, int, dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
int (*readlink) (struct dentry *, char *, int);
int (*follow_link) (struct dentry *, struct nameidata *);
int (*put_link) (struct dentry *, struct nameidata *);
void (*truncate) (struct inode *);
int (*permission) (struct inode *, int);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,
const void *, size_t, int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*removexattr) (struct dentry *, const char *);
};
  • create - 创建一个新的磁盘索引节点
  • lookup - 查找索引节点所在的目录
  • unlink - 从 dir 目录删除 dentry 目录项所指文件的硬链接

Entity

  • File
    • 不建议使用
    • File 含义太过宽泛
    • File 可能和系统里对象冲突
  • FileMeta - 文件的基本元数据
    • 用于快速检索和显示文件列表时的基本信息
    • name, size, type
  • FileMetadata - 文件的详细元数据
    • 显示文件的详细信息和管理文件的权限、版本等
    • author, tags, description, version, permissions
  • FileRef - 文件引用信息
    • 指向实际存储位置的引用或链接
    • path, object_url
  • FileContent - 文件内容
    • 实际存储的文件内容
    • 用于 dedup
    • content, hash
  • FileInfo
  • FileEntry - 文件记录
    • 包含 path 信息
    • 用于表示文件在系统中的位置
    • path, parent, children
  • DirectoryEntry - 目录记录
    • 包含 path 信息
    • 用于表示目录在系统中的位置
    • path, parent, children

  • 常见配对关系
    • FileEntry & DirectoryEntry - 目录结构
    • FilePath & FileMeta
    • FileRef & FileContent - 存储分离

PHP

flystorage

export type CommonStatInfo = Readonly<{
path: string;
lastModifiedMs?: number;
visibility?: string;
}>;

export type FileInfo = Readonly<
{
type: 'file';
size?: number;
isFile: true;
isDirectory: false;
mimeType?: string;
} & CommonStatInfo
>;

export type DirectoryInfo = Readonly<
{
type: 'directory';
isFile: false;
isDirectory: true;
} & CommonStatInfo
>;

export type StatEntry = FileInfo | DirectoryInfo;

export type FileContents = Iterable<any> | AsyncIterable<any> | NodeJS.ReadableStream | Readable | string;

export interface StorageAdapter {
write(path: string, contents: Readable, options: WriteOptions): Promise<void>;
read(path: string): Promise<FileContents>;
deleteFile(path: string): Promise<void>;
createDirectory(path: string, options: CreateDirectoryOptions): Promise<void>;
copyFile(from: string, to: string, options: CopyFileOptions): Promise<void>;
moveFile(from: string, to: string, options: MoveFileOptions): Promise<void>;
stat(path: string): Promise<StatEntry>;
list(path: string, options: { deep: boolean }): AsyncGenerator<StatEntry>;
changeVisibility(path: string, visibility: string): Promise<void>;
visibility(path: string): Promise<string>;
deleteDirectory(path: string): Promise<void>;
fileExists(path: string): Promise<boolean>;
directoryExists(path: string): Promise<boolean>;
publicUrl(path: string, options: PublicUrlOptions): Promise<string>;
temporaryUrl(path: string, options: TemporaryUrlOptions): Promise<string>;
checksum(path: string, options: ChecksumOptions): Promise<string>;
mimeType(path: string, options: MimeTypeOptions): Promise<string>;
lastModified(path: string): Promise<number>;
fileSize(path: string): Promise<number>;
}

export type MiscellaneousOptions = {
[option: string]: any;
};

export type MimeTypeOptions = MiscellaneousOptions & {
disallowFallback?: boolean;
fallbackMethod?: 'contents' | 'path';
};

export type VisibilityOptions = {
visibility?: string;
directoryVisibility?: string;
};
export type WriteOptions = VisibilityOptions &
MiscellaneousOptions & {
mimeType?: string;
size?: number;
cacheControl?: string;
};

export type CreateDirectoryOptions = MiscellaneousOptions & Pick<VisibilityOptions, 'directoryVisibility'> & {};
export type PublicUrlOptions = MiscellaneousOptions & {};
export type CopyFileOptions = MiscellaneousOptions &
VisibilityOptions & {
retainVisibility?: boolean;
};
export type MoveFileOptions = MiscellaneousOptions &
VisibilityOptions & {
retainVisibility?: boolean;
};
export type ListOptions = { deep?: boolean };
export type TemporaryUrlOptions = MiscellaneousOptions & {
expiresAt: ExpiresAt;
};

Object Storage

  • Hash 分层
  • CAS (Content Addressable Storage) 内容可寻址存储系统
    • hash -> object
    • kv
  • FCS - fixed content storage

git

garage

  • metadata
  • data
    • {2}/{2}/hash.zst

juicefs

  • chunk 64 MiB
  • slice -> object 4 MiB
    • sliceId 为 int
  • meta - 以 Redis 为例
    • setting - string
    • allSessions - sset
    • sessionInfos - hash
    • Node i${inode} - string
    • Edge/Dir d${inode} - hash{fn,binary}
    • LinkParent p${inode} - hash
    • Chunk c${inode}_${index} - list
    • SliceRef k${sliceId}_${size} - hash
    • Symlink s${inode} - string
    • Xattr x${inode} - hash
    • Flock lockf${inode} - hash{${sid}_${owner},R/W}
    • Plock lockp${inode} - hash
    • DelFiles - delfiles - sset
    • DelSlices - delSlices - hash
    • Sustained - session${sid} - list
  • data
    • ${fsname}/chunks/${hash}/${basename}
      • hash
        • 有Prefix -> fmt.Sprintf("%02X/%d", sliceId%256, sliceId/1000/1000)
        • 无Prefix -> fmt.Sprintf("%d/%d", sliceId/1000/1000, sliceId/1000)
      • basename -> ${sliceId}_${index}_${size}
  • 参考
type setting struct {
Name string `xorm:"pk"`
Value string `xorm:"varchar(4096) notnull"`
}
type counter struct {
Name string `xorm:"pk"`
Value int64 `xorm:"notnull"`
}
type node struct {
Inode Ino `xorm:"pk"`
Type uint8 `xorm:"notnull"`
Flags uint8 `xorm:"notnull"`
Mode uint16 `xorm:"notnull"`
Uid uint32 `xorm:"notnull"`
Gid uint32 `xorm:"notnull"`
Atime int64 `xorm:"notnull"`
Mtime int64 `xorm:"notnull"`
Ctime int64 `xorm:"notnull"`
Nlink uint32 `xorm:"notnull"`
Length uint64 `xorm:"notnull"`
Rdev uint32
Parent Ino
AccessACLId uint32 `xorm:"'access_acl_id'"`
DefaultACLId uint32 `xorm:"'default_acl_id'"`
}
type edge struct {
Id int64 `xorm:"pk bigserial"`
Parent Ino `xorm:"unique(edge) notnull"`
Name []byte `xorm:"unique(edge) varbinary(255) notnull"`
Inode Ino `xorm:"index notnull"`
Type uint8 `xorm:"notnull"`
}
type chunk struct {
Id int64 `xorm:"pk bigserial"`
Inode Ino `xorm:"unique(chunk) notnull"`
Indx uint32 `xorm:"unique(chunk) notnull"`
Slices []byte `xorm:"blob notnull"`
}
type sliceRef struct {
Id uint64 `xorm:"pk chunkid"`
Size uint32 `xorm:"notnull"`
Refs int `xorm:"notnull"`
}
type symlink struct {
Inode Ino `xorm:"pk"`
Target []byte `xorm:"varbinary(4096) notnull"`
}
type xattr struct {
Id int64 `xorm:"pk bigserial"`
Inode Ino `xorm:"unique(name) notnull"`
Name string `xorm:"unique(name) notnull"`
Value []byte `xorm:"blob notnull"`
}
type flock struct {
Id int64 `xorm:"pk bigserial"`
Inode Ino `xorm:"notnull unique(flock)"`
Sid uint64 `xorm:"notnull unique(flock)"`
Owner int64 `xorm:"notnull unique(flock)"`
Ltype byte `xorm:"notnull"`
}
type plock struct {
Id int64 `xorm:"pk bigserial"`
Inode Ino `xorm:"notnull unique(plock)"`
Sid uint64 `xorm:"notnull unique(plock)"`
Owner int64 `xorm:"notnull unique(plock)"`
Records []byte `xorm:"blob notnull"`
}
type delfile struct {
Inode Ino `xorm:"pk notnull"`
Length uint64 `xorm:"notnull"`
Expire int64 `xorm:"notnull"`
}
type delslices struct {
Id uint64 `xorm:"pk chunkid"`
Deleted int64 `xorm:"notnull"`
Slices []byte `xorm:"blob notnull"`
}
type sustained struct {
Id int64 `xorm:"pk bigserial"`
Sid uint64 `xorm:"unique(sustained) notnull"`
Inode Ino `xorm:"unique(sustained) notnull"`
}

Web File System API

  • Chrome 86+
  • FileSystemHandle
  • FileSystemFileHandle
  • FileSystemDirectoryHandle
  • FileSystemSyncAccessHandle
  • FileSystemWritableFileStream
  • Window.showOpenFilePicker()
  • Window.showSaveFilePicker()
  • Window.showDirectoryPicker()
  • DataTransferItem.getAsFileSystemHandle()
  • StorageManager.getDirectory()
  • input webkitdirectory - <input type='file' webkitdirectory multiple>
    • Chrome 7+, Safari 11.1+
    • 能获取到 webkitEntries - <input type='file' multiple>
      • Chrome 22+, Safari 11.1+
interface FileSystemHandle {
readonly kind: 'file' | 'directory';
readonly name: string;

isSameEntry(other: FileSystemHandle): Promise<boolean>;
queryPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise<PermissionState>;
requestPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise<PermissionState>;

/**
* @deprecated Old property just for Chromium <=85. Use `kind` property in the new API.
*/
readonly isFile: boolean;

/**
* @deprecated Old property just for Chromium <=85. Use `kind` property in the new API.
*/
readonly isDirectory: boolean;
}
var FileSystemHandle: {
prototype: FileSystemHandle;
new (): FileSystemHandle;
};
type FileSystemHandleUnion = FileSystemFileHandle | FileSystemDirectoryHandle;

interface FilePickerAcceptType {
description?: string | undefined;
accept: Record<string, string | string[]>;
}

interface FilePickerOptions {
types?: FilePickerAcceptType[] | undefined;
excludeAcceptAllOption?: boolean | undefined;
}

interface OpenFilePickerOptions extends FilePickerOptions {
multiple?: boolean | undefined;
}

interface SaveFilePickerOptions extends FilePickerOptions {
suggestedName?: string;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface DirectoryPickerOptions {}

type FileSystemPermissionMode = 'read' | 'readwrite';

interface FileSystemPermissionDescriptor extends PermissionDescriptor {
handle: FileSystemHandle;
mode?: FileSystemPermissionMode | undefined;
}

interface FileSystemHandlePermissionDescriptor {
mode?: FileSystemPermissionMode | undefined;
}

interface FileSystemCreateWritableOptions {
keepExistingData?: boolean | undefined;
}

interface FileSystemGetFileOptions {
create?: boolean | undefined;
}

interface FileSystemGetDirectoryOptions {
create?: boolean | undefined;
}

interface FileSystemRemoveOptions {
recursive?: boolean | undefined;
}

type WriteParams =
| { type: 'write'; position?: number | undefined; data: BufferSource | Blob | string }
| { type: 'seek'; position: number }
| { type: 'truncate'; size: number };

type FileSystemWriteChunkType = BufferSource | Blob | string | WriteParams;

// TODO: remove this once https://github.com/microsoft/TSJS-lib-generator/issues/881 is fixed.
// Native File System API especially needs this method.
interface WritableStream {
close(): Promise<void>;
}

class FileSystemWritableFileStream extends WritableStream {
write(data: FileSystemWriteChunkType): Promise<void>;
seek(position: number): Promise<void>;
truncate(size: number): Promise<void>;
}

interface FileSystemFileHandle extends FileSystemHandle {
readonly kind: 'file';
getFile(): Promise<File>;
createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>;
/**
* @deprecated Old property just for Chromium <=85. Use `kind` property in the new API.
*/
readonly isFile: true;
/**
* @deprecated Old property just for Chromium <=85. Use `kind` property in the new API.
*/
readonly isDirectory: false;
}

var FileSystemFileHandle: {
prototype: FileSystemFileHandle;
new (): FileSystemFileHandle;
};

interface FileSystemDirectoryHandle extends FileSystemHandle {
readonly kind: 'directory';
getDirectoryHandle(name: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle>;
getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>;
removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<void>;
resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null>;
keys(): AsyncIterableIterator<string>;
values(): AsyncIterableIterator<FileSystemDirectoryHandle | FileSystemFileHandle>;
entries(): AsyncIterableIterator<[string, FileSystemDirectoryHandle | FileSystemFileHandle]>;
[Symbol.asyncIterator]: FileSystemDirectoryHandle['entries'];
/**
* @deprecated Old property just for Chromium <=85. Use `kind` property in the new API.
*/
readonly isFile: false;
/**
* @deprecated Old property just for Chromium <=85. Use `kind` property in the new API.
*/
readonly isDirectory: true;
}

var FileSystemDirectoryHandle: {
prototype: FileSystemDirectoryHandle;
new (): FileSystemDirectoryHandle;
};

interface DataTransferItem {
getAsFileSystemHandle(): Promise<FileSystemHandle | null>;
}

interface StorageManager {
getDirectory(): Promise<FileSystemDirectoryHandle>;
}

function showOpenFilePicker(
options?: OpenFilePickerOptions & { multiple?: false | undefined },
): Promise<[FileSystemFileHandle]>;
function showOpenFilePicker(options?: OpenFilePickerOptions): Promise<FileSystemFileHandle[]>;
function showSaveFilePicker(options?: SaveFilePickerOptions): Promise<FileSystemFileHandle>;
function showDirectoryPicker(options?: DirectoryPickerOptions): Promise<FileSystemDirectoryHandle>;

// Old methods available on Chromium 85 instead of the ones above.

interface ChooseFileSystemEntriesOptionsAccepts {
description?: string | undefined;
mimeTypes?: string[] | undefined;
extensions?: string[] | undefined;
}

interface ChooseFileSystemEntriesFileOptions {
accepts?: ChooseFileSystemEntriesOptionsAccepts[] | undefined;
excludeAcceptAllOption?: boolean | undefined;
}

/**
* @deprecated Old method just for Chromium <=85. Use `showOpenFilePicker()` in the new API.
*/
function chooseFileSystemEntries(
options?: ChooseFileSystemEntriesFileOptions & {
type?: 'open-file' | undefined;
multiple?: false | undefined;
},
): Promise<FileSystemFileHandle>;
/**
* @deprecated Old method just for Chromium <=85. Use `showOpenFilePicker()` in the new API.
*/
function chooseFileSystemEntries(
options: ChooseFileSystemEntriesFileOptions & {
type?: 'open-file' | undefined;
multiple: true;
},
): Promise<FileSystemFileHandle[]>;
/**
* @deprecated Old method just for Chromium <=85. Use `showSaveFilePicker()` in the new API.
*/
function chooseFileSystemEntries(
options: ChooseFileSystemEntriesFileOptions & {
type: 'save-file';
},
): Promise<FileSystemFileHandle>;
/**
* @deprecated Old method just for Chromium <=85. Use `showDirectoryPicker()` in the new API.
*/
function chooseFileSystemEntries(options: { type: 'open-directory' }): Promise<FileSystemDirectoryHandle>;

interface GetSystemDirectoryOptions {
type: 'sandbox';
}

interface FileSystemDirectoryHandle {
/**
* @deprecated Old property just for Chromium <=85. Use `.getFileHandle()` in the new API.
*/
getFile: FileSystemDirectoryHandle['getFileHandle'];

/**
* @deprecated Old property just for Chromium <=85. Use `.getDirectoryHandle()` in the new API.
*/
getDirectory: FileSystemDirectoryHandle['getDirectoryHandle'];

/**
* @deprecated Old property just for Chromium <=85. Use `.keys()`, `.values()`, `.entries()`, or the directory itself as an async iterable in the new API.
*/
getEntries: FileSystemDirectoryHandle['values'];
}

interface FileSystemHandlePermissionDescriptor {
/**
* @deprecated Old property just for Chromium <=85. Use `mode: ...` in the new API.
*/
writable?: boolean | undefined;
}

API

Homedir

LanguageCode
Cconst char *homedir = getenv("HOME") ? getenv("HOME") : getpwuid(getuid())->pw_dir;
Node.jsconst homedir = os.homedir();
JavaString homedir = System.getProperty("user.home");
Pythonhomedir = os.path.expanduser("~")
Gohomedir, _ := os.UserHomeDir()
golang.org/x/tools/godoc/vfs
package vfs // import "golang.org/x/tools/godoc/vfs"

import (
"io"
"os"
)

// RootType indicates the type of files contained within a directory.
//
// It is used to indicate whether a directory is the root
// of a GOROOT, a GOPATH, or neither.
// An empty string represents the case when a directory is neither.
type RootType string

const (
RootTypeGoRoot RootType = "GOROOT"
RootTypeGoPath RootType = "GOPATH"
)

// The FileSystem interface specifies the methods godoc is using
// to access the file system for which it serves documentation.
type FileSystem interface {
Opener
Lstat(path string) (os.FileInfo, error)
Stat(path string) (os.FileInfo, error)
ReadDir(path string) ([]os.FileInfo, error)
RootType(path string) RootType
String() string
}

// Opener is a minimal virtual filesystem that can only open regular files.
type Opener interface {
Open(name string) (ReadSeekCloser, error)
}

// A ReadSeekCloser can Read, Seek, and Close.
type ReadSeekCloser interface {
io.Reader
io.Seeker
io.Closer
}

libc

  • dirent.h
  • unistd.h
struct dirent {
ino_t d_ino; /* Inode编号 */
off_t d_off; /* 不是偏移量,详见下文 */
unsigned short d_reclen; /* 此记录的长度 */
unsigned char d_type; /* 文件类型,不是所有文件系统都支持 */
char d_name[256]; /* 以空字符结尾的文件名 */
};


#define DT_UNKNOWN 0 // Unknown file type
#define DT_FIFO 1 // FIFO (named pipe)
#define DT_CHR 2 // Character device - 字符设备
#define DT_DIR 4 // Directory - 目录
#define DT_BLK 6 // Block device - 块设备
#define DT_REG 8 // Regular file - 文件
#define DT_LNK 10 // Symbolic link - 符号链接
#define DT_SOCK 12 // Socket - 套接字
#define DT_WHT 14 //

int alphasort(const struct dirent **, const struct dirent **);
int closedir(DIR *);
int dirfd(DIR *);
DIR *fdopendir(int);
DIR *opendir(const char *);
ssize_t posix_getdents(int, void *, size_t, int);
struct dirent *readdir(DIR *);
int readdir_r(DIR *restrict, struct dirent *restrict, struct dirent **restrict);
void rewinddir(DIR *);
int scandir(const char *, struct dirent ***, int (*)(const struct dirent *), int (*)(const struct dirent **, const struct dirent **));
void seekdir(DIR *, long);
long telldir(DIR *);

Explorer

  • Explorer 通常会实现一些额外的元数据
    • 例如: 作者、标签、描述、版本、本地化
  • 功能
    • macOS Finder 收藏 Favorites
    • Windows Quick Access
    • 书签 Bookmarks / Starred
      • Windows XP
      • nemo
      • Files
    • 快捷方式(Shortcuts)
  • File Explorer / File Manager

Windows

  • desktop.ini
  • attrib +s "C:\path\to\folder"
  • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FolderDescriptions
    • CLSID LocalizedString
desktop.ini
[.ShellClassInfo]
LocalizedResourceName=显示名称
LocalizedResourceName=@%SystemRoot%\system32\shell32.dll,-21770

macOS

  • 扩展属性 Extended Attributes
    • com.apple.FinderInfo
    • com.apple.metadata
  • 本地化目录结构
    • .localized
    • .lproj
      • e.g. zh-CN.lproj/InfoPlist.strings
        • CFBundleDisplayName = "显示名称";

Linux

  • GNOME、KDE、XDG
  • .directory
  • Desktop Entry
  • ~/.config/user-dirs.dirs
  • xdg-user-dirs-update ~/.config/user-dirs.locale
    • 语言代码
.directory
[Desktop Entry]
Name=显示名称
Name[zh_CN]=显示名称

Glossory

  • path - 完整路径 /home/user/documents/report.pdf
    • path=dirname+basename
  • filename - 文件名 report.pdf
    • name+extname
      • extname 包含 .
    • ^[^\\/:*?"<>|]+(\.[a-zA-Z0-9]+)?$
    • reserved
  • dirname - 目录名 /home/user/documents
  • basename - 指路径中最后一部分的名称,无论它是文件还是目录
  • stem
    • basename w/o extname

参考