大师网-带你快速走向大师之路 解决你在学习过程中的疑惑,带你快速进入大师之门。节省时间,提升效率

深度文件管理器技术填坑笔记

单例模板设计

c++里面我们经常使用单例对象,特别是在做全局对象的时候,我们希望程序里边就只有一个单例。实现一个单例的程序例如下面:

class A{
    public:
    static A* instance{
        static A instance;
        return &insance;
    }
    private:
    A();
    ~A();
    A(const A &);
    A & operator = (const A &);
}

但是在一个项目里边我们有很多个单例对象,那岂不是每个类都要写一遍实例化函数?我们可以做一个单例模板:


template<typename T>
class Singleton
{
public:
    static T* instance(){
        static T instance;
        return &instance;
    }

private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton &);
    Singleton & operator = (const Singleton &);
};

这时候我们就可以初始化各种类的全局单例对象了:

#define globalA Singleton<A>::instance()
#define globalB Singleton<B>::instance()
globalA;
globalB;

对于这样的模板单例,我感觉能让代码更加规范,可读性也比较好,在这记录下来了。

单进程多窗口管理

文件管理器是一个但进程多窗口应用。所谓但进程多窗口,就是不管打开了多少个应用窗口,它其实都是在一个独立进程里边,在打开新窗口的时候我们并没有因此开辟了一个新进程。多窗口实现还是蛮简单的,但进程就针对不同平台有不同的解决办法(windows,linux的解决方法都有不同)这里主要针对linux下如何实现单例应用。在linux下要实现单例应用,方法有挺多的,然而有些方法是不怎么靠谱(特别使用QSharedMemery来实现单例应用时候,在程序正常运行的时候加锁是正常的,正常退出程序的时候解锁也是正常的,但是直接杀掉这个进程,就没法解锁了,后面就不能正常启动程序了)不管怎么样,这里列下两种暂时没发现有问题的方式实现单例应用:

    1. 通过QLocalSocket/QLocalServer监听服务连接(windows,linux都适用)

使用QLocalSocket/QLocalServer实现一个单例应用实现的过程如上图结构。我们来一步步讲解它的逻辑是怎么走的。按着这个结构,我们首先在启动程序之前,先用QLocalSocket跟服务端进行连接,如果连接失败了,这时候证明当前的进程就是唯一的单例。然后我们用QLocalServer去监听我们规定好的地址,等待这下一个同样的进程启动的时候接受发送过来的数据进行命令处理;如果连接成功了,证明系统已经有了一个单例进程,我们通过对Server端写命令数据(这个数据是自定义的,这样我们就可以跟单例进程通讯,让创建新窗口,而不是开一个新进程来创建新窗口),然后退出该进程。如此循环,不管我们启动多少次程序,都只有一个进程在跑着啦!

接下来我们看一下QLocalSocket/QLocalServer做了什么。首先QLocalServer监听服务,它的地址名字是自定义的,比如我们的文件管理器地址叫做dde-file-manager,然后在XDG_RUNTIME_DIR里边创建一个地址名字socket文件,然后在这个地址上监听sokect请求。接着是QLocalSocket连接服务,地,如址也是这个自定义的名字果有服务监听这个地址,那么Socket是能够正常建立连接的,如果没有,连接将会失败。我们看一下目录截图:

(昨晚太困睡着了,今天继续写)

关于local doman socket c编程文章

当程单例程序退出来了,那么我们的QLocalServer监听将会被释放内存,这时候并不会造成像QsharedMemery那样造成假锁的问题。

    2. 通过DBus服务进程监听(只使用于Linux系统)

一个Qt 的MVC构架模式

由于文件管理器这个项目有点庞大,我感觉在于MVC构架方面有点混,不好抽出来分析。这里就用个人的方法去实现了。

Controller View MenuAction 框架

基于Qt的信号-槽机制,其实我们可以很灵活地把前端和后端进行数据分离。但是过度的使用这个框架的构建代码结构,在用于高复杂的的程序设计中还是有缺陷的。因为在复杂程序中我们经常会遇到某个模块跟某个模块是有比较大的依赖关系,然而过度的把它们分离出来,这样在后期维护或者扩展的时候就比较困难了。这里的代码例子实现先留空,后面再填上去。

右键菜单自动化,Qt元对象特性(路由设计)

Qt的Meta-Object-System 的功能好像都是不是很大收到关注,然而它这个特性给我们做程序开发提供了非常大的帮助。下面根据Qt官方文档的解释说明Qt 元对象系统的概念。

Qt的元对象系统系统为内部对象之间的通讯,运行时类型信息和动态属性系统提供了一套信号-槽机制。Qt的元对象系统基于三件事情:
1. Object 提供了一个使用元对象系统的基础功能类。

  1. 在类的私有区使用Q_OBJECT宏来使能元对象系统的功能,比如动态属性,信号-槽等。

  2. Meta-Object Compiler (moc) 在编译过程中把所有基础QObject类的对象进行预编译,生成元对象功能所需的代码块。

这是Qt元对象从声明实例化到编译的三个要点,当然我们在编码的时候基本不需要关系moc这块干了什么,我们只需关系怎么灵活使用qt元对象系统。我们在使用Qt库的时候应该已经很成熟的使用信号-槽机制来做业务逻辑处理了这里主要介绍后面两个特性的使用(动态属性和运行时类型信息)。

Qt属性系统

在Qt元对象功能中,属性系统是一个运行时动态的,我们可以在任何时候对元对象增加一个属性,也可以去掉一个属性。Qt元对象中一共setProperty()函数插入一个或者修改已有属性,property()函数读取属性值。通过这个功能,我们在做程序开发的时候,可以不用自己去死写getter/setter之类的属性,直接就在需要的时候给元对象附加一个属性,读取一个属性。这个操作是非常方便的,我们可以减少硬编码的量,做做成自动化:很多时候我们要硬编码存取数据结构,经常我们是这么干的:

class A{

public:
    int a(){return m_a;}
    void setA(int a){m_a = a;}
    int b(){return m_b;}
    void setB(int b){m_b = b;}
    .
    .
    .

private:
        int m_a;
        int m_b;
        .
        .
        .
}

然后我们做很多模块开发,都要复写这些getter/setter,写多了经常不注意规范,然后我们的代码可读性都边困难了。还有一种情况是在开发之后,我们没有给类A硬编码属性b(假设类A有好多getter/setter属性),然后我们做拓展功能的时候不得不给类A存一个b属性,而且b属性就在某个逻辑被使用了一次。很多时候,我们因为这些“纸片”不得不在“书本”里边临时性乱插进去(一般只有形成了强迫症的人才会把他排版的漂亮)。但是当我们有了Qt元对象的运行时动态属性之后,就方便多了。我们来看看一个简单的例子:


class A : public QObject{
    Q_OBJECT
    void init(){
        setProperty("a", 1);
        setProperty("b", "hello");
        setProperty("c", false);
    }
}

int main(){
    
    A a;
    //读取a对象的属性“a”
    cout<< a.property("a",1);
    //动态增加新的属性
    a.setProperty("newProperty", "value hello")
    return 0;
}

两个程序对比,在多个类的设计中它们的差别就会变得更加们明显了。当然属性系统里边也有一定的限制,比如我们的属性值是QVariant所支持的可以转化,至于用户自定义的数据结构还没有研究过。不过很多情况下,QVariant所支持的数据类型已经足够我们使用这个特性来做基于动态属性系统的程序设计了。

QMetaObject 和 QMetaEnum

QMetaObject是Qt里面很强大的一个元对象信息类,它可以把一个类里边的数据结构序列化。所谓序列化就是把类里边的属性,函数,枚举都能全部转化出一个我们设定的字符数据结构(数组,json,链表等等)。除了这个,QMetaObject还能主动出发一个对象的槽函数,和监听信号触发。QMetaEnum是一个枚举转Map<key,value>的工具,能够把我们定义的枚举转化成字符串来使用。结合QMetaObject和QMetaEnum我们来尝试设计一个路由:

void DFileMenuManager::actionTriggered(QAction *action)
{
    DFileMenu *menu = qobject_cast<DFileMenu *>(sender());
    DFMEvent event = menu->event();
    event << DFMEvent::Menu;
    if (action->data().isValid()){
        bool flag = false;
        int _type = action->data().toInt(&flag);
        MenuAction type;
        if (flag){
            type = (MenuAction)_type;
        }else{
            qDebug() << action->data().toString();;
            return;
        }

        //通过传过来的枚举转化成字符串
        QMetaEnum metaEnum = QMetaEnum::fromType<MenuAction>();
        QString key = metaEnum.valueToKey(type);
        QString methodKey = QString("action%1").arg(key);
        QString methodSignature = QString("action%1(" QT_STRINGIFY(DFMEvent) ")").arg(key);

        const QMetaObject* metaObject = appController->metaObject();
        
        //通过字符串触发一个函数
        if (metaObject->indexOfSlot(methodSignature.toLocal8Bit().constData()) != -1){
            QMetaObject::invokeMethod(appController,
                                      methodKey.toLocal8Bit().constData(),
                                      Qt::DirectConnection,
                                      Q_ARG(DFMEvent, event));
        }else{
            qWarning() << "Appcontroller has no method:" << methodSignature;
        }
    }
}

上面代码是文件管理器右键菜单处理工作的构架。我们的右键菜单是使用了枚举存放功能值,当要触发这个动作的时候,我们可以避免硬编码一个个if语句判断来定死调用一个动作函数。在这里设计一个路由,把动作枚举自动匹配到一个函数中,让代码逻辑更加简洁。

缩略图生成与管理

缩略图标准原文

如今文件管理器的缩略图支持图片,视频,文本,pdf。图片和文本的缩略图都是自绘(或者Qt内部实现的,图片会增加格式插件支持),视频使用的是ffmpeg的libffmpegthumbnailer实现的,pdf一开始是用了poppler-qt5实现,后来这个库有bugs,无法修复,然后就使用了gio的glibpoppler,最后不得不直接用原生libpoppler才能把pdf格式的缩略图片稳定生成了。
poppler
libffmpegthumbnailer
缩略图生成与管理遵守了freeeDesktop标准来实现的,所有遵守freeDesktop标准的第三方应用都能共用一套缩略图管理库。

提权实现

freeDesktop官方标准文档

提权配置文件(以深度格式化工具为例子):

com.deepin.pkexec.usb-device-formatter.policy

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
 "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
  <vendor>LinuxDeepin</vendor>
  <vendor_url>https://www.deepin.com/</vendor_url>
  <action id="com.deepin.pkexec.usb-device-formatter"> <-- 设置应用ID -->
    <message>Authentication is required to run the Usb Device Formatter</message>
    <icon_name>usb-device-formatter</icon_name>
    <defaults>
      <allow_any>no</allow_any>
      <allow_inactive>no</allow_inactive>
      <allow_active>yes</allow_active> <-- 设置激活验证模式:no,yes,auth_self,auth_admin,auth_self_keep,auth_admin_keep -->
    </defaults>
    <annotate key="org.freedesktop.policykit.exec.path">/usr/bin/usb-device-formatter</annotate> <-- 设置程序执行路径 -->
    <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate> <-- 设置是否允许GUI -->
  </action>

</policyconfig>

配置文件目录:

/usr/share/polkit-1/actions/

启动方式:

    $pkexec usb-device-formatter

samba文件共享

官方Fucking menual

安装

    $sudo apt-get intall samba

配置

与之前我们使用的配置不一样,这次使用的是net这个工具进行控制的(samba内置的一个工具,强悍好用),所以配置的是用户环境,而不是修改samba配置文件,比如我的用户名是tommego:

  • 添加用户组

        $sudo groupadd sambashare
  • 添加用户到用户组

        $sudo useradd tommego sambashare
  • 设置samba共享密码

        $sudo smbpasswd -a tommego -s
  • 注销一下生效

控制与使用

列出已共享的文件

列出共享配置详细信息

net 工具特征

  • 所在目录:

    /var/lib/samba/usershares
    
  • 配置文件目录下的文件

    一个共享文件夹对应一个配置文件,配置文件名字为共享文件名
  • 配置文件内容格式


    * path:共享文件夹路径

    • sharename:共享名

    • guest_ok:是否可以匿名登陆

    • usershare_acl:访问权限设置(R为只读,F为允许所有操作,D为拒绝操作)

    基本使用就是这些了,高级使用请自行参考funcking manual

文件管理器加入这个功能的解决方案:

  • 提权设置用户组,添加用户,设置密码等

  • 主动监听/var/lib/samba/usershares目录,读取解析配置文件,实时监听,控制和更新数据。

文件获取图标实现

linux下支持使用多主题功能,我们可以切换各种有没的主题,每个主题包都是对应的图标资源文件。文件管理器根据当前主题拿到对应的图标资源。

目录

    /usr/share/icons

配置文件

index.theme
[Icon Theme]
Name=deepin
Comment=Default icon theme for deepin
Inherits=flattr

Example=x-directory-normal

Directories=actions/scalable,actions/22,actions/24,apps/48,apps/128,apps/scalable,devices/scalable,devices/48,places/22,places/24,places/32,places/48,places/64,places/128,places/scalable,status/scalable,status/48,mimetypes/22,mimetypes/24,mimetypes/32,mimetypes/48,mimetypes/64,mimetypes/128,mimetypes/scalable

[actions/scalable]
Size=96
Context=Actions
Type=Scalable
MinSize=16
MaxSize=96

[actions/22]
Size=22
Context=Actions
Type=Fixed

[actions/24]
Size=24
Context=Actions
Type=Fixed

[apps/48]
Size=48
Context=Applications
Type=Fixed

[apps/128]
Size=128
Context=Applications
Type=Fixed

[apps/scalable]
Size=128
Context=Applications
Type=Scalable
MinSize=16
MaxSize=512

[emblems/12]
Size=12
Context=Emblems
Type=Fixed

[emblems/22]
Size=22
Context=Emblems
Type=Fixed

[emblems/24]
Size=24
Context=Emblems
Type=Fixed

[emblems/32]
Size=32
Context=Emblems
Type=Fixed

[emblems/48]
Size=48
Context=Emblems
Type=Fixed

[emblems/scalable]
Size=96
Context=Emblems
Type=Scalable
MinSize=16
MaxSize=128

[devices/scalable]
Size=256
Context=Devices
Type=Scalable
MinSize=16
MaxSize=256

[devices/48]
Size=48
Context=Devices
Type=Fixed

[emblems/scalable]
Size=24
Context=Emblems
Type=Fixed

[mimetypes/22]
Size=22
Context=MimeTypes
Type=Fixed

[mimetypes/24]
Size=24
Context=MimeTypes
Type=Fixed

[mimetypes/32]
Size=32
Context=MimeTypes
Type=Fixed

[mimetypes/48]
Size=48
Context=MimeTypes
Type=Fixed

[mimetypes/64]
Size=64
Context=MimeTypes
Type=Fixed

[mimetypes/128]
Size=128
Context=MimeTypes
Type=Fixed

[mimetypes/scalable]
Size=512
Context=MimeTypes
Type=Scalable
MinSize=16
MaxSize=512

[places/22]
Size=22
Context=Places
Type=Fixed


[places/24]
Size=24
Context=Places
Type=Fixed

[places/32]
Size=32
Context=Places
Type=Fixed

[places/48]
Size=48
Context=Places
Type=Fixed

[places/64]
Size=64
Context=Places
Type=Fixed

[places/128]
Size=128
Context=Places
Type=Fixed


[places/scalable]
Size=256
Context=Places
Type=Scalable
MinSize=16
MaxSize=256

[status/48]
Size=48
Context=Status
Type=Fixed

[status/scalable]
Size=96
Context=Status
Type=Scalable
MinSize=16
MaxSize=96

主题图标类型

  • apps 应用程序

  • mimetypes 文件类型

  • cursor 鼠标

  • places

  • emblems

  • actions 动作

  • devices 设备

  • status 状态
    文件管理器拿的主题图标包括apps, mimetypes两个。

类似获取图标过程(某些步骤是Qt和glib实现的)

回收站

路径:

~/.local/share/Trash

~/.local/share/Trash/files

~/.local/share/Trash/info

linux下的回收站主要由这两个组成,files目录存这源文件原有的内容,info目录存着垃圾文件的信息,一个垃圾文件对应着一个.trashinfo文件:

path为原来的路径
DeletionDate为删除时间

avfs直读技术引入

avfs参考文献
fuse参考文献
avfs是一个压缩文件快速读取工具,它在挂载系统之后,会把整个磁盘目录做了一个映射,映射的根目录为:

~/.avfs

当我们要快速预览压缩文件内容的时候,我们可以直接在这个映射目录下找得到。

安装

$sudo apt-get install avfs

挂载

$mountavfs

映射规则(假设有一个压缩文件a.zip,在/home/tommego/test目录下):

  1. 从~/.avfs开始,映射压缩文件原有目录路径:

    ~/.avfs
  2. 当访问压缩文件内容时候,在文件压缩文件后缀后面加一个"#"号,然后必须在"#"号后面加"/":

    ~/.avfs/home/tommego/test/a.zip#/
    
    

经过了处理后,就能实现在文件管理器中实现视图显示和操作了。

压缩文件直读不支持格式(不包括压缩错误格式文件)

.ar
.cbz
.exe
iso
.tar.7z
.tar.lz
.tar.lzo

压缩文件直读中文乱码格式:

.cpio

格式化工具实现

格式化工具里边本身支持格式化成许多种格式,由于需求问题,u盘格式化工具只支持Ntfs, Fat16, Fat32三种,这里相对全面解释格式化工具部分支持格式的格式化实现:

获取各种格式磁盘容量和使用空间信息(全部以/dev/sdb1为磁盘例子)

  • Btrfs

    $ sudo btrfs filesystem show /dev/sdb1
  • Efi, Fat16, Fat32

    $ sudo dosfsck -n -v /dev/sdb1
  • Ext2, Ext3, Ext4

    $ sudo dumpe2fs -h /dev/sdb1 
  • Jfs

    $ sudo sh -c echo dm | jfs_debugfs /dev/sdb1
  • Nilfs2

    $ sudo nilfs-tune -l /dev/sdb1
  • Ntfs

    $ sudo ntfsinfo -mf /dev/sdb1
  • Reiser4

    $ sudo debugfs.reiser4 --force --yes /dev/sdb1
  • Reiserfs

    $ sudo debugreiserfs -d /dev/sdb1
  • Xfs

    $ sudo xfs_db -c sb -c print -r /dev/sdb1
    

linux 文件系统格式一览目录源码

将磁盘格式化为各种格式磁盘(全部以/dev/sdb1为磁盘例子)

  • Btrfs

    $ sudo mkfs.btrfs -f labelname /dev/sdb1
  • Efi, Fat32

    $ sudo mkfs.msdos -F32 -v -I labelname /dev/sdb1
  • Ext2

    $ sudo mkfs.ext2 -F labelname /dev/sdb1
  • Ext3

    $ sudo mkfs.ext3 -F labelname /dev/sdb1
  • Ext4

    $ sudo mkfs.ext4 -F labelname /dev/sdb1
  • F2fs

    $ sudo mkfs.f2fs labelname /dev/sdb1
  • Fat16

    $ sudo mkfs.msdos -F16 -v -I labelname /dev/sdb1
  • Hfs

    $ sudo hformat labelname /dev/sdb1
  • Hfsplus

    $ sudo mkfs.hfsplus labelName /dev/sdb1
  • Jfs

    $ sudo mkfs.jfs -q labelName /dev/sdb1
  • Linuxswap

    $ sudo mkswap labelName /dev/sdb1
  • Nilfs2

    $ sudo mkfs.nilfs2 labelName /dev/sdb1
  • Ntfs

    $ sudo mkntfs -Q -v -F -L labelName /dev/sdb1
  • Reiser4

    $ sudo mkfs.reiser4 --force --yes labelName /dev/sdb1
  • Reiserfs

    $ sudo mkreiserfs -f -f labelName /dev/sdb1
  • Xfs

    $ sudo mkfs.xfs -f labelName /dev/sdb1
    

综合使用效果

实现说明:

由于u盘格式化工具是一个独立的进程,但是要满足对话框形式,而且对当前文件管理器窗口进行模态,所以使用了x11的api做了特殊处理:

在调度格式化工作时候,由于需要root权限,所以这里的格式化工具是做提权处理的,提权的实现在上面已经提到(哈哈,像我这菜鸟新手,写这个工具还是很兴奋的,毕竟整个过程学习到了不少东西呀!).

dbus服务

文件系统监听实现

rlocate快速文件搜索实现

网络文件系统

文件预览

插件机制实现

无线分享(后期)

手机助手(后期)

文件管理器hack小技术