gdfs: 基于Fuse的GoogleDrive客户端开源代码分析

背景

在学习fuse的过程中,首先从libfuse中的demo开始学习,以了解用户态与内核态通信的框架。而此处的demo只聚焦于最基本的通信,用户态文件系统的实现只是一个最简单的read only文件系统,其他操作都是假接口。

要继续深入学习,直接看cephfs等高集成、高完善的代码容易被细节淹没,最好能够循序渐进,同时也可以横向对比,看各个实现解决了什么问题,如何在用户态组织文件索引,缓存如何实现,客户端与服务端的缓存一致性如何解决,分布式锁如何实现,实现方式是否优雅,哪里有需要完善的地方等。

找到一个基于fuse开发的google drive文件系统,翻了一下代码结构比较简单,适合入门学习。因此本文以该项目来做说明。

项目地址:
GitHub - robin-thomas/GDFS: Google Drive File System

注:该代码涉及到通过api与google drive进行交互,因此也需要了解google api:

gdrive api:
https://developers.google.com/drive/api/guides/about-sdk?hl=zh-cn

google drive go sdk quickstart(此处以go sdk为例。官方不提供c++ sdk,因此作者是通过libcurl手搓的api调用):
https://developers.google.com/drive/api/quickstart/go?hl=zh-cn

google cloud console:
https://console.cloud.google.com/apis/credentials?hl=zh-cn&inv=1&invt=AbpcxA&project=wangxz-proj00049

环境搭建

bash 复制代码
# 拉起centos8容器:
IMAGE_ID='5d0da3dc9764'
NAME=centos8_demo
docker run --privileged -idt \
        --name $NAME \
        -v /data:/data \
        --net host \
        ${IMAGE_ID} \
        /usr/sbin/init
docker exec -it $NAME /bin/bash

# 配置源:
mkdir /etc/yum.repos.d/orig && mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/orig && curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-8.repo

# 安装编译工具:
yum install automake gcc-c++ cmake libfuse-devel libfuse3-devel curl libcurl -y -q

# 编译:
# 按照具体按照的automake版本重新配置,否则会报错找不到automake-1.15:
autoreconf -f -i
./configure
make 
make install

# 配置:
vim /opt/gdfs/gdfs.conf
# 日志位置:
tail -f /opt/gdfs/gdfs.log

# 启动:
/opt/gdfs/gdfs.sh start

# 或
/usr/local/bin/mount.gdfs -m /tmp/robin -f -s --log_path /opt/gdfs --log_level DEBUG -o allow_other -o allow_root -o direct_io

b mount_fs
b get_node
b gdfs_read
b fuse_ll_process_buf
b LRUCache::get
b File::get
b File::read_file

#--
cat /tmp/robin/_opt_gdfs_/haha

架构分析

google auth

该项目使用google OAuth2.0进行鉴权。API示例如下:

javascript 复制代码
# google api示例:
curl 'https://www.googleapis.com/drive/v3/about?fields=storageQuota(limit%2CusageInDrive)&key=[YOUR_API_KEY]' \
  --header 'Authorization: Bearer [YOUR_ACCESS_TOKEN]' \
  --header 'Accept: application/json' \
  --compressed

api模拟:https://developers.google.com/drive/api/reference/rest/v3/about/get?hl=zh-cn&apix_params=%7B%22fields%22%3A%22storageQuota(limit%2CusageInDrive)%22%7D

google drive/v3/files api

需要了解如何使用google api来访问google drive的文件。

● 每个文件或文件夹都有一个file_id作为唯一标识。例如可以通过以下接口来获取某个file_id的修改时间:

javascript 复制代码
https://www.googleapis.com/drive/v3/files/<file_id>?fields=modifiedTime

gdfs使用该接口来查看某个文件的修改时间是否与本地相同。若不同,则意味着远端有修改,需要获取这个目录所有的子目录(否则无法知道子目录是否被修改),然后查看其子项是否被修改。若有修改,需要GET下来。

需要完整的新子项列表才能准确识别出被删除的文件(存在于旧列表但不在新列表中的项)

需要完整列表来检测和处理文件名冲突(当多个客户端并发创建同名文件时)

若父目录没有被修改,则也意味着其子目录下的文件也没有被修改。

一个包含5000个文件的目录需要分5次请求获取当云端删除10个文件时,仅通过修改时间无法知道具体删除了哪些文件当其他客户端新增文件时,需要通过完整列表同步新增项

javascript 复制代码
// Construct the URL to send the request.
url  = GDFS_FILE_URL_ + std::string("?pageSize=1000&q='") + parent_file_id;
url += "'+in+parents+and+trashed+%3D+false&orderBy=name&spaces=drive";
url += "&fields=files(id%2CmimeType%2CmodifiedTime%2Cname%2Csize%2CviewedByMeTime)%2CnextPageToken";

数据结构

公共功能

javascript 复制代码
//一个全局静态变量file_id_node,用于维护file_name与GDFSNode指针的映射关系:
std::unordered_multimap <std::string, GDFSNode *> file_id_node;
// 这样设计在多客户端挂载的情况下应该会有问题,TODO:测试。

// 一个客户端的挂载点对应一个GDrive,包含了uid/gid,容量信息,挂载目录名称rootDir,用于和google api调用的Auth实例,缓存管理LRUCache实例,线程池实例,以及一个GDFSNode *root指针,在GDrive::get_root()的时候初始化,指向当前文件系统的根目录。并将其加入file_node_id的map中。
class GDrive {
  public:
    uid_t uid;
    gid_t gid;
    time_t mounting_time;
    uint64_t bytes_used;
    uint64_t bytes_free;
    uint64_t bytes_total;
    std::string rootDir; //根目录地址
    std::string change_id; //根目录change_id,可以从远端通过API获取扼杀,用于描述根目录是否被修改
    Auth auth; //认证实例
    LRUCache cache; //缓存管理实例
    Threadpool threadpool; //线程池管理实例
    struct GDFSNode * root; //根目录GDFSNode指针
}

// 类比于inode用于描述一个文件的基础数据结构,可以用其找到父节点(parent),子节点(通过map children)。
// (linux是将file_name放到entry中,但本项目是直接将其放到Node中。)
struct GDFSNode {
  char link;
  std::string file_name;
  std::string sym_link;
  GDFSEntry * entry;  // 该文件到entry信息指针
  GDFSNode  * parent; // 父节点指针
  std::unordered_map <std::string, struct GDFSNode *> children;// 该目录下的子node map
}

//用于描述一个文件在内存中的组织形式,类比于dentry
struct GDFSEntry {
  std::string file_id; //google drive中的文件标识,每个文件有唯一的file_id,所有通过api交互,都需要用其来进行标识。
  //file_id的生成规则在下面描述。
  uint64_t file_size;
  time_t ctime;
  time_t mtime;
  time_t atime;
  time_t cached_time;
  uid_t uid;
  gid_t gid;
  mode_t file_mode;
  dev_t dev;
  bool is_dir; //是否为dir,用于区分处理文件夹与文件
  int ref_count;
  std::string mime_type;
  bool g_doc;
  bool dirty;
  bool pending_create;//在创建文件的时候,标记是否为
  bool file_open;
  bool write;
  bool pending_get;
}

sem_t req_item_sem;
pthread_mutex_t worker_lock;
std::list <struct req_item> req_queue;
std::queue <std::string> file_id_q; //全局的file_id队列,批量生成

file_id的生成

在GDrive::make_file()中,若file_name以'.'开头,则表示隐藏文件,file_id定义为file_id = gdfs_name_prefix + rand_str();否则表示正常文件或文件夹,此时从file_id_q中找到一个作为file_id。

此处的队列file_id_q是在每次GDrive::make_file()或GDrive::make_dir()时主动调用generate_file_id()生成的。这个队列预期是存放大于100个file_id队列。

当在创建文件或文件夹的的时候,会调用generate_file_id(),检查该队列是否为空,或该队列小于100个,若是,则调用API 'generateIds'来获取,file_id被放到该队列'file_id_q'中,

javascript 复制代码
void
GDrive::make_file (const std::string & file_name,
                   mode_t file_mode,
                   struct GDFSNode * parent_node,
                   uid_t uid_,
                   gid_t gid_)
void
GDrive::make_dir (const std::string & file_name,
                  mode_t file_mode,
                  struct GDFSNode * parent_node,
                  uid_t uid_,
                  gid_t gid_)

pending_create:延时远端创建

当客户端创建文件时,在GDrive::make_file()中,获取上文所述的file_id,new一个GDFSEntry(),将其加入到parent_node中,并将其加入file_id_node中:

javascript 复制代码
// Add to the directory tree.
entry = new GDFSEntry(file_id, 0, false, mtime, mtime, uid_, gid_, file_mode);
assert(entry != NULL);
node = parent_node->insert(new GDFSNode(file_name, entry, parent_node));
assert(node != NULL);
file_id_node.emplace(file_id, node);

判断,若不是系统文件(即文件名不以gdfs_name_prefix开头),而是用户自己的文件,则将该entry->pending_create设置为true,表示正在创建过程中,然后向google api发起一个INSERT请求:

javascript 复制代码
if (file_id.compare(0, gdfs_name_prefix.size(), gdfs_name_prefix) != 0) {
  entry->pending_create = true;//设置pending_create=true,表示正在创建。
  threadpool.build_request(file_id, INSERT, node, url, query);  
}

在调用google api的INSERT请求完成后,将其设置为false,表示针对该文件的创建请求已经完成:

javascript 复制代码
bool
Threadpool::send_insert_req (std::string & url,
                             std::string & query,
                             struct GDFSEntry * entry) {
  //...
  //发送请求:
  resp = this->auth.sendRequest(url, INSERT, query);
  //更新mtime:
  mtime = rfc3339_to_sec(val["modifiedTime"].get());
  entry->mtime = entry->ctime = mtime;
  //将pending_create置回false:
  entry->pending_create = false; 
  //...
}

这样设计是因为内存中建立GDFSNode数据结构与发请求给Google Drive上传二者是不原子的。

那么在什么时候会用到这个entry->pending_create呢?

在get_children()中,最后一步会检查GoogleDrive现有的文件list与

get_children:建立数据结构、按需更新修改到本地

javascript 复制代码
/*
 * Retrieve the children list,
 * given a pointer to the node of the directory,
 * in the directory tree.
 */
void
GDrive::get_children (struct GDFSNode * parent)

在GDrive::get_children(struct GDFSNode *parent)中,会根据传入的parent来查询其子项,若内存中没有node,会根据远端获取的列表及信息进行重建。同时还会检查是否远端与本地不一致,若远端比本地新,则拉取最新的文件,并更新node。

具体会进行如下工作:

● 检查该目录是否被修改,未同步到本地。即是否为pending_get状态。若parent->entry->pending_get==true,则重置其为false,然后设置dir_modified=true。

○ 设置dir_modified=true后,后续会判断,若非如此,则意味着这个目录下没有被修改,无需更新。

○ 设置pending_get状态的时机为get_children()中,判断当前entry为dir(is_dir==true),且entry->mtime < mtime(entry中记录的mtime小于从接口中获取的该文件的mtime,即该目录在远端被修改过,还没有更新到客户端)。

○ pending_get只会在dir类型的entry中设置,不会在file级别设置。

● 若当前传入的parent为根目录(parent->file_name=="/"

○ 调用"https://www.googleapis.com/drive/v3/changes/startPageToken?fields=startPageToken"获取change_id,与当前GDrive中记录的change_id做对比。

○ 若不同,则意味着根目录被修改过,则设置dir_modified=true,并更新新的change_id到GDrive->change_id。

● 以上两种条件都不进入,则进入当前条件,即当前文件夹不是根目录,可以是某个子目录或者文件,

○ 调用"https://www.googleapis.com/drive/v3/files/<parent_fild_id>/?fileds=modifiedTime",来获取当前file_id的mtime。

○ 若获取到的mtime大于entry中记录的mtime(mtime> entry->mtime),或当前parent没有字目录时,设置dir_modified=true。

■ 此处判断parent->is_empty()==true时,也认为dir可能被修改,是因为可能这个目录下远端有新建文件,而还没有同步到本地。需要强制校验。

● 若dir_modified==false, 则意味着本parent_node没有被修改,直接goto out。否则意味着perent_node已被修改,继续现有流程。

以上都是处理parent本身的判断,以下将处理parent作为目录时,其字目录和文件的事项。

● 调用API,获取最新的parent的子项列表,传值给child_items。

这里的获取列表是以1000个为分页处理的,类似rgw中的分页list对象,以避免单个目录文件数目过大,占用大量内存,以及查询返回超时。

javascript 复制代码
url  = GDFS_FILE_URL_ + std::string("?pageSize=1000&q='") + parent_file_id;
url += "'+in+parents+and+trashed+%3D+false&orderBy=name&spaces=drive";
url += "&fields=files(id%2CmimeType%2CmodifiedTime%2Cname%2Csize%2CviewedByMeTime)%2CnextPageToken";

● 遍历child_items

○ 获取其file_id和mtime。记录atime=mtime。

○ 检查child是否是dir。根据child中的mimeType判断是否为dir(if (mime_type == "application/vnd.google-apps.folder")),若是,则设置is_dir=true。

○ 若不是dir,则检查是否为Google Docs,若是,则在后缀加pdf。

javascript 复制代码
 if (mime_type == "application/vnd.google-apps.document" ||
      mime_type == "application/vnd.google-apps.spreadsheet" ||
      mime_type == "application/vnd.google-apps.drawing" ||
      mime_type == "application/vnd.google-apps.presentation") {
    g_doc = true;
    file_name += ".pdf";

○ 若既不是dir,又不是Google Docs,只是普通文件,则根据返回的size字段,赋值给file_size。

○ 根据file_id,查看map file_id_node中是否存在。

■ 若存在,则获取其entry。

● 若该文件为脏数据,即entry->dirty==true,则跳过。

○ 设置entry->dirty=true的时机为:删除某个文件时,在GDrive::delete_file(GDFSNode _node, bool delete_req)(此处还涉及到硬连接的处理)

■ 若node->parent存在,则断开其父节点到本节点的指针链接。

■ 若当前节点是dir,则遍历所有children,将其加入q_nodes待处理队列中。

■ 否则不是目录,但entry->ref_count==1,则将其调用cache.remove(file_id)删除。

■ 若被删除的不是特殊文件(gdfs_name_prefix),且有请求删除标记(delete_req),且(entry->ref_count==1或entry是dir且ref_count==2,此时标记为脏:entry->dirty=true,并将其加入队列处理:threadpool.build_request(file_id, DELETE, node, url);

■ 否则意味着有硬连接,即多个inode有相同file_id。此时需要注意删除正确的node。

● 在file_id_node中根据file_id找到对应GDFSNode,进行遍历,找到当前node对应的项目,并从file_id_node删除。

■ 释放node内存,并将其设为NULL。

○ ref_count初始化时,dir为2,文件为1.

○ ref_count减少时机:~GDFSNode()析构函数中。

○ ref_count增加时机:在gdfs_link(path,new_path)中

■ 找到new_path对应的new_parent和new_file_name

■ 根据path,uid,pid调用state->get_node()赋值给node,并获取到entry=node->entry;

■ 校验,若entry->is_dir=true,则返回EPERM,不允许创建硬连接给目录。

■ 根据new_parent,uid,gid通过state->get_node()获取node,确认new_parent存在。

■ 检查权限

■ 检查新路径是否存在:tmp_node=node->find(new_file_name)

■ 增加entry的引用计数:++entry->ref_count

■ 针对new_file_name,新建一个new GDFSNode,并将其加到file_id_node中,注意此处file_id还是老的file_id。即实现了硬连接。

● 若该文件正在被写入,即entry->write=true,则将其file_id加入集合s1中。

○ entry->write被设置为true的时机1:gdfs_write(_path, *buf, size, offset, fuse_file_info)中:

■ 根据path获取node: node=state->get_node(path,uid,gid);

■ 根据node获取entry: entry=node->entry;

■ 检查权限

■ 设置当前时间为entry->mtime, 设置entry->file->size

■ 将当前文件进行缓存,详见《写入缓存逻辑》:

javascript 复制代码
ret = state->cache.put(entry->file_id, const_cast<char*>(buf), offset, size, node, false);
entry->write = true;

■ 返回size给write系统调用。

○ entry->write被设置为true的时机2: gdfs_truncate(path,newsize):

■ 获取node = state->get_node(path,uid,gid);

■ 获取entry = node->entry;

■ 判断newsize大小

● 若newsize==0,则清空缓存中该file_id的缓存:

state->cache.put(entry->file_id, NULL, 0, 0, NULL);

● 否则是truncate指定大小,则如同write操作一样,设置写标记:entry->write=true。然后计算truncate的起始位置与大小,调用memset初始化给定大小的buf,更新到缓存中。

○ 若newsize > entry->file_size,则补零:

state->cache.put(entry->file_id, buf, start, size, node, false)

○ 否则,若newsize < entry->file_size,则对缓存进行缩减:state->cache.resize(entry->file_id, newsize);

■ 更新缓存中该file_id的mtime: state->cache.set_time(entry->file_id, mtime);

■ 跟新entry->mtime与entry->file_size。

● 若该文件是Google Docs,即g_doc==true,且远端的文件被修改未同步到本地,即mtime>entry->mtime,则调用download_file()将其下载到本地。

● 若该文件是dir,即is_dir==true,且被远端修改未同步到本地,则设置entry->pending_get=true。

● 判断file_name是否与本地记录的file_name相同

○ 若名字相同,则更新entry中的atime与mtime

○ 否则处理名字冲突。并更新entry中的atime与mtime。

■ 若不存在file_id_node中,则需要重新建立GDFSEntry和GDFSNode数据结构,将其插入parent的GDFSNode链表中,并下载到本地。

这也是对于首次访问某个文件时,客户端本地没有对应文件,通过本函数调用该parent文件夹的list接口,找到这个文件,建立客户端的元数据接口,并将数据拉到本地的位置。

■ 将该file_id加入到s1中。

○ 从parent->get_children()中,获取所有的child,加入s2中。

○ 对s1,s2做集合差,写入s3中

○ 遍历s3,判断是否是deleted_child,加入其中

○ 遍历deleted_child,调用child->parent->remove_child(child_node),从GDFSNode角度删除该node。

file_node_id:

file_id_node是一个允许重复键的哈希表(unordered_multimap<string, GDFSNode*>),用于存储Google Drive文件ID到节点指针的映射。

添加时机:

在make_dir(),make_file(),get_children()等需要建立内存数据结构时添加。

读取时机:

唯一的作为检查是在get_children()中,用于检查该file_id是否在本地。若在,则进行一些操作;若不在,则new GDFSNode与GDFSEntry,重新建立内存数据结构。

其他场景则是单纯用于遍历,delete或renam操作时的修改操作。

修改时机:

rename。

删除时机:

delete。

get_node实现

实现功能为根据path进行路径查询,从缓存中找到对应的GDFSNode。若不在缓存中,则调用get_children构建并加入缓存。

javascript 复制代码
struct GDFSNode *
GDrive::get_node (const std::string & path,
                  uid_t uid,
                  gid_t gid,
                  bool search)

● 在GDrive实例中,找到root node指针,这是整个查找树的起点:struct GDFSNode * node = this->root;

● 将传入的path赋值给tmp,判断其是否以'/'结尾

○ 若tmp本身就是'/',则意味着是根目录。调用get_children(node)获取其子目录,并goto out;

○ 否则意味着不是根目录,将结尾的'/'去掉。tmp.pop_back();

● 对tmp进行循环根据'/'分割,逐步解析路径上的文件名称到next_dir,

○ 在父node中,根据分割出来的下一层目录尝试查找GDFSNode: child = node->find(next_dir),即根据file_name在node->children中查询,看是否存在。

■ 若不存在,则调用GDrive::get_children(node)进行重建。然后再进行查询:child = node->find(next_dir)。若还查不到,则报错目录不存在。

■ 否则意味着找到了下级目录,node = child;

■ 检查权限: GDrive->file_access(uid,gid,X_OK,node->entry);

■ 若不是目录,则报错返回。node->entry->is_dir == false;

● 处理队列中pending DELETE请求。若node->entry->dirty==true,则意味着正在队列中等待删除,此时直接返回不存在,并goto out;

● 若未在缓存中找到,则调用get_children(node),从GoogleDrive中查找,并new GDFSNode与GDFSEntry,加入缓存:

javascript 复制代码
// Check if the path component exists.
child = node->find(next_dir);
if (child == NULL) {
  search = ((search && tmp.empty() == true) ? true : false);
  try {
    this->get_children(node);
  } catch (GDFSException & err) {
    err_num = errno;
    error = err.get();
    goto out;
  }
  child = node->find(next_dir);
  if (child == NULL) {
    err_num = ENOENT;
    error = "path component " + next_dir + " does not exist";
    goto out;
  }
}
node = child;

● 一些update_node(node)或get_children(node)的逻辑。

javascript 复制代码
if (search &&
  node != NULL &&
  node->entry->mtime > 0 &&
  node->entry->file_id.compare(0, gdfs_name_prefix.size(), gdfs_name_prefix) != 0 &&
  (node->entry->is_dir || node->link == 0) &&
  node->entry->write == false) {
this->update_node(node);
} else if (node != NULL &&
         node->entry->is_dir &&
         node->entry->pending_get) {
get_children(node);
}

缓存管理数据结构

由以下三个数据结构管理:

Page: 数据存储单元

File:文件级缓存管理。一个文件由一个File管理,一个File可能由多个不同(start,stop)的Page组成。这样设计可以实现大文件的部分加载,以降低内存等资源占用。

LRUCache:系统级缓存管理

javascript 复制代码
// 一个文件的某个部分
struct Page {       // 数据存储单元
    char* mem;      // 内存块指针
    off_t start;    // 起始偏移
    off_t stop;     // 结束偏移
    size_t size;    // 数据块长度
};

struct File {       // 文件级管理,针对某个文件,由多个pages的set组成
    std::set<Page*> pages;  // 有序分页集合
    pthread_mutex_t lock;   // 文件级锁
    size_t size;            // 文件总大小
    time_t mtime;           // 修改时间
};

class LRUCache {    // 全局缓存管理,每个GDFSDrive拥有一个实例
    std::list<std::pair<std::string, File*>> cache; // 访问顺序链表
    std::unordered_map<std::string, decltype(cache.begin())> map; // 文件索引
};

缓存读取逻辑:LRUCache::get

javascript 复制代码
size_t
LRUCache::get (const std::string & file_id,
               char * buffer,
               off_t offset,
               size_t len,
               struct GDFSNode * node)

基本逻辑如下:

● 根据传入的file_id,在LRUCache->map中查找:

○ 若没有,则新建一个File空实例,将其加到LRUCache->cache前端。并加到LRUCache->map中。通过当前给定的file_id做关联。

○ 若能找到,则将对应的 File对象移动到cache的前端(通过cache.splice())。若传入的to_delete为true,则说明所有file page都被从google drive中下载下来了,整个文件可能都被修改了,将其全部移除(f->delete_pages())。

● 释放缓存里传入的len大小的空间,以确保缓存有容量。 this->free_cache(len);

● 将buffer复制到新建的new_buf,通过调用File->put(new_buf, start,stop, node->entry),将其加入到缓存中。

● 更新LRUCache->size长度:this->size += len;

而此处File->put接口的实现则有点意思,涉及到一个File如何通过多个Page来优化管理空间。

将Page写入File:File::put

javascript 复制代码
struct Page *
File::put (char * buf,
           off_t start,
           off_t stop,
           struct GDFSEntry * entry)

如上文所述,File用于表示一个文件。而针对大文件部分读取的场景,为了优化空间和网络带宽占用,采用了类似于range的方式进行管理:每个File都由多个Page组成,其数据结构为

std::set <struct Page *, page_cmp> pages; 其中page_cmp定义为a->start < b->start,即以start升序排序的、key唯一的map。put相同的key的value,也无法修改其内容。

此处主要有以下几种可能性:

javascript 复制代码
# 如下有两个page,[start,stop]分别为[99,199], [299,399]。中间为空,表示客户端缓存了该文件的这两个range。
# 此时有以下7种可能性:
           |99--------199|                |299--------399|
1) |20-80|
2) |20-------------150|
3) |20-----------------------230|
4)           |100--150|
5)           |100------------230|
6)           |100----------------------------350|
7)                           |200---280| 

其中,第1、2、3种可能性可以被第5、6、7包含,因此最终剩下如下4种可能性:

javascript 复制代码
           |99--------199|                |299--------399|
1)           |100--150|
2)           |100------------230|
3)           |100----------------------------350|
4)                           |200---280| 

以下分别对其进行解释:

  1. 新修改内容全部在原来的某个Page的某个Range之内:直接覆盖这个Page的指定Range的内容为新修改内容。
  2. 新修改的内容部分在某个Page的某个Range之内,但超出一部分。超出的部分是空隙:在这个Page之内的,修改。在这个Page之外的,new Page,加入File。
  3. 在2)的基础上,超出的部分覆盖了下个Page的一部分:在这个Page之内的修改,在这个Page之外的,new Page并加入File,覆盖下个Page的,修改。
  4. 新修改的内容在两个已有的Page的空隙内:直接new Page,加入File。
    5)这个File原来没有page:这种最简单,直接new Page加入File即可。
    如此便可以使用一个File->pages来管理某个文件的部分数据的缓存,可以做到"需要哪一部分才取哪一部分"的效果,避免数据的全量拷贝。

将File写入缓存:LRUCache::put

该函数实现了LRU缓存写入机制。主要包括:

● 维护LRU淘汰顺序

● 缓存空间管理

● 文件数据页更新

javascript 复制代码
bool
LRUCache::put (const std::string & file_id,
               char * buffer,
               off_t offset,
               size_t len,
               struct GDFSNode * node,
               bool to_delete) 

具体思路如下:

● 在cache->map中查找,若找不到,意味着不在缓存中,则需要new File,将其与file_id关联,加入map中。

● 否则意味着能在缓存中找到,则调整cache队列,使用splice函数,将该File放到缓存队列前端,即LRU效果。判断是否to_delete为true,若是,则意味着缓存在已经是脏数据,需清空缓存,调用f->delete_pages()。

to_delete只在cache.put()的时候根据实际场景被设置。设置的时机有如下:

○ to_delete=true的场景:

■ GDrive::download_file()。该方法只针对GoogleDoc。可能该格式有特殊要求,不做过多探讨。

■ gdfs_truncate()中,newSize==0时,即需要全清的场景。

○ to_delete=false的场景:

■ gdfs_truncate(),newSize !=0时,即部分truncate的场景。

■ gdfs_write()。

● 根据传入的len,释放缓存空间,以避免内存不足。

● 调用File->put(),将Page加入cache。

● 更新cache->size。

javascript 复制代码
//eg:
(gdb) p file_id_node
$49 = std::unordered_multimap with 2 elements = {["1wKdLp3fiUv3fKVj6ibTnFhUv1MSMOwmY"] = 0x13046f0, ["root"] = 0x1221520}

FUSE接口实现

理解了《公共功能》部分,实现FUSE接口就十分简单了。举例说明:

javascript 复制代码
# 以 echo "123" >  robin/_opt_gdfs_/1234 为例
# 先调用get_attr

# gdfs_create
state->make_file() # 在Google Drive中创建文件。涉及获取parent_file_id与当前文件的file_id,调用API在Google Drive中创建文件,建立GDFSEntry与GDFSNode,将其加入parent结构中,将<file_id,node>加入到file_id_node map中。

# gdfs_open


# gdfs_write
## 将数据加入缓存
ret = state->cache.put(entry->file_id, const_cast<char*>(buf), offset, size, node, false);
entry->write = true;  
# gdfs_release
## 获取node,entry:
 node = state->get_node(path, uid, gid);
 entry = node->entry;
 
## 检查access:
 
## 上传到Google Drive:
state->write_file(node); # 调用UPLOAD API上传。做了些优化,使用GDFS_UPLOAD_CHUNK_SIZE进行分片上传,但是用法上只有简单的重试
entry->write = false;
entry->file_open = false;

libfuse框架

javascript 复制代码
#4  0x00007fdabc02b818 in fuse_ll_process_buf () from /lib64/libfuse.so.2
#5  0x00007fdabc028013 in fuse_session_loop () from /lib64/libfuse.so.2
#6  0x00007fdabc0200b6 in fuse_loop () from /lib64/libfuse.so.2
#7  0x00007fdabc030a87 in fuse_main_common () from /lib64/libfuse.so.2
#8  0x0000000000410cf4 in initGDFS (rootDir="/tmp/robin", path="/opt/gdfs/", argc=6, argv=0x7ffcc2bca2e0) at gdfs.cc:1612
#9  0x0000000000406df8 in mount_::mount_fs () at mount.cc:142
#10 0x00000000004064fc in main (argc=15, argv=0x7ffcc2bca578) at main.cc:172

api认证

javascript 复制代码
class Auth {
  private:
    Request reqObj;
}
class Request {
  private:
    std::string confFile;
    std::string redirectUri;
    std::string clientId;
    std::string clientSecret;
    std::string accessToken;
    std::string refreshToken;
    time_t expiresIn;
}
enum requestType {
  GET,
  POST,
  DELETE,
  UPDATE,
  INSERT,
  DOWNLOAD,
  UPLOAD_SESSION,
  UPLOAD,
  GENERATE_ID,
};
shell 复制代码
#0  GDrive::get_node (this=0x9c1ce0, path="/_opt_gdfs_/haha", uid=0, gid=0, search=false) at gdapi.cc:126
#0  gdfs_read (path=0xaf08c0 "/_opt_gdfs_/haha", buf=0x7fdabc84f010 "", size=131072, offset=0, fi=0x7ffcc2bc9e80) at gdfs.cc:1310
#1  0x00007fdabc0224e8 in fuse_fs_read_buf () from /lib64/libfuse.so.2
#2  0x00007fdabc0226b6 in fuse_lib_read () from /lib64/libfuse.so.2
#3  0x00007fdabc02b0df in do_read () from /lib64/libfuse.so.2
#4  0x00007fdabc02b818 in fuse_ll_process_buf () from /lib64/libfuse.so.2
#5  0x00007fdabc028013 in fuse_session_loop () from /lib64/libfuse.so.2
#6  0x00007fdabc0200b6 in fuse_loop () from /lib64/libfuse.so.2
#7  0x00007fdabc030a87 in fuse_main_common () from /lib64/libfuse.so.2
#8  0x0000000000410cf4 in initGDFS (rootDir="/tmp/robin", path="/opt/gdfs/", argc=6, argv=0x7ffcc2bca2e0) at gdfs.cc:1612
#9  0x0000000000406df8 in mount_::mount_fs () at mount.cc:142
#10 0x00000000004064fc in main (argc=15, argv=0x7ffcc2bca578) at main.cc:172

存在问题

● 没有考虑多客户端并发修改删除,即没有实现分布式锁。

● 没有本地客户端与google drive的强一致性处理。应该是最终一致性,不是强一致性。

附录

原有out-of-band的auth方式已经在2023年被完全屏蔽:

2022 年 2 月 28 日 - OOB 流程禁止使用新的 OAuth

2022 年 9 月 5 日 - 系统可能会向不合规的 OAuth 请求显示一条面向用户的警告消息

2022 年 10 月 3 日 - 对于 2022 年 2 月 28 日之前创建的 OAuth 客户端,OOB 流程已废弃

2023 年 1 月 31 日 - 所有现有客户端都被屏蔽(包括豁免的客户端)
https://developers.google.com/identity/protocols/oauth2/resources/oob-migration?hl=zh-cn

相关推荐
别说我什么都不会2 小时前
鸿蒙轻内核M核源码分析系列十七(3) 异常信息ExcInfo
操作系统·harmonyos
AWS官方合作商1 天前
AWS S3深度解析:十大核心应用场景与高可用架构设计实践
云计算·aws·数据湖·对象存储·存储·s3
a小胡哦1 天前
Windows、Mac、Linux,到底该怎么选?
linux·windows·macos·操作系统
别说我什么都不会2 天前
鸿蒙轻内核M核源码分析系列十五 CPU使用率CPUP
操作系统·harmonyos
袁庭新2 天前
CentOS7通过yum无法安装软件问题解决方案
centos·操作系统
别说我什么都不会3 天前
鸿蒙轻内核M核源码分析系列十二 事件Event
操作系统·harmonyos
qq_437896434 天前
动态内存分配算法对比:最先适应、最优适应、最坏适应与邻近适应
操作系统
别说我什么都不会4 天前
鸿蒙轻内核M核源码分析系列十一 (2)信号量Semaphore
操作系统·harmonyos
别说我什么都不会4 天前
鸿蒙轻内核M核源码分析系列十 软件定时器Swtmr
操作系统·harmonyos