分类 Linux 下的文章

最近接触了两款开源、跨平台、支持多种SQL数据库的数据库管理工具,值得记录一下。

DBeaver
官网:https://dbeaver.io/
2020年疫情期间,在家办公,想找个数据库管理工具,可以在Linux上访问SQL Server数据库,于是遇到DBeaver。界面像Eclipse,容易上手;基于Java,可以跨平台使用;使用JDBC,几乎支持所有数据库。在Linux上,几乎是万能的数据库管理工具了。

HeidiSQL
官网:https://www.heidisql.com/
在Windows上安装MariaDB 10.4.12时,发现自带了HeidiSQL数据库管理工具。界面及操作都跟MySQL Workbench相似,清晰明了,而且支持各种SQL数据。比较意外的是,其基于Delphi开发,所以Linux上需要利用Wine运行。

昨天发现Debian服务器上的Aria2居然不能下载https的链接,才发现编译安装时,忘了设置开启SSL的参数。还是记录一下,以免后面又犯错了。

关于编译安装的教程,可以直接查看官方说明:
https://github.com/aria2/aria2/blob/master/README.rst
https://aria2.github.io/manual/en/html/README.html#how-to-build

1. 安装相关依赖

详见官方文档。注意的是,Linux上,开启SSL,要安装openssllibssl-dev

2. 编译

官方文档已经很详细了,总结脚本如下:

$ git clone https://github.com/aria2/aria2.git
$ cd aria2
$ ./configure --without-gnutls --with-openssl
$ make
$ sudo cp ./src/aria2c /usr/local/bin/

3. 部署服务

关于Aria2配置文件的说明,参考官方文档:
https://aria2.github.io/manual/en/html/aria2c.html#aria2-conf
示例配置文件如下(参考路径:/etc/aria2/aria2.conf):

#OPTIONS
#下载路径
dir=/opt/aria2_download
#log路径
log=/var/log/aria2/aria2.log
#log-level: debug, info, notice, warn or error
log-level=warn
console-log-level=warn
#session
input-file=/var/cache/aria2/aria2.session
#最大下载数,默认5
max-concurrent-downloads=5
#校验完整性,只在bt下有效果,默认false
check-integrity=true
#断点续传
continue=true
 
#HTTP/FTP/SFTP Options
#同时连接的服务器数量,默认1
max-connection-per-server=5
#最大尝试次数,默认5
max-tries=20
#最小文件分割大小,默认20M
#min-split-size=20M
#单个文件最大线程,默认5
#split=5
#超时时间,默认60
#timeout=60
 
#BitTorrent Specific Options
#启用本地发现
bt-enable-lpd=true
#hash校验种子,默认true
bt-hash-check-seed=true
#最大打开文件数量,默认100
bt-max-open-files=200
#单个种子最大连接数
bt-max-peers=100
#在磁力下载中,保留torrent文件
bt-save-metadata=true
#监听端口,默认6881-6999
#listen-port=6881-6999
#最大上传限制,0是无限制
max-overall-upload-limit=100K
#下载完成后做种的设置
seed-ratio=1.0
seed-time=120
#bt-tracker=需要相关的服务地址
 
#RPC Options
#启用rpc
enable-rpc=true
#允许所有访问
rpc-allow-origin-all=true
#监听所有网络
rpc-listen-all=true
#监听端口
rpc-listen-port=6800
#rcp保存上传的元数据,默认false
rpc-save-upload-metadata=true
 
#Advanced Options
#下载时覆盖已经存在的文件,默认false
allow-overwrite=false
#此选项为true可能会导致下载进度丢失,默认false
allow-piece-length-change=true
#总是尝试恢复下载,默认true
always-resume=true
#指定dns服务器
#async-dns=true
#async-dns-server=8.8.4.4,208.67.222.222
#如果文件存在,自动重命名,仅适用于http,ftp
auto-file-renaming=true
#自动保存间隔,控制文件保存在.aria2中
auto-save-interval=60
#作为守护进程启用
daemon=true
#禁用ipv6
disable-ipv6=true
#磁盘缓存,默认16M
disk-cache=16M
#文件是否启用预先分配,默认prealloc
file-allocation=falloc
#最大下载结果在内存中保留数量,默认1000
max-download-result=500
#最大失败重试次数,默认0
max-resume-failure-tries=0
#下载完成时候执行的脚本
#on-bt-download-complete=/etc/aria2/on-bt-download-complete
#on-download-complete=/etc/aria2/on-download-complete
#on-download-error=/etc/aria2/on-download-error
#总体下载速度限制
max-overall-download-limit=1024K
#单个下载最大速度限制
max-download-limit=1024K
#保存下载进度,很有用的配置
save-session=/var/cache/aria2/aria2.session
#保存间隔,默认0
save-session-interval=60

# token验证
rpc-secret=123456

Systemd的服务配置文件(参考路径:/etc/systemd/system/aria2.service):

[Unit]
Description=Aria2 Service
After=network.target

[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/var/cache/aria2
ExecStart=/usr/local/bin/aria2c --conf-path=/etc/aria2/aria2.conf -D
ExecReload=/usr/bin/kill -HUP $MAINPID
RestartSec=1min
Restart=on-failure

[Install]
WantedBy=multi-user.target

4. 客户端

我用yaaw,纯静态页面,服务器上部署个Nginx即可:
https://github.com/binux/yaaw

某天接触到某个用Golang实现的程序,不仅体积小,还支持几乎所有种类的CPU,于是下决心学习一下Go这门语言。虽然刚出来的时候就想学,但那时据说有很多坑(比如语法可能会变),就放弃了。现在连被成为垃圾的包管理也有了升级,觉得是时候去学。恰好要把一个二级域名绑定IP的小服务迁移到VPS上,于是干脆用Go重新实现(原来是用Python3)这个服务。

首先要阅读相关教程。初学者教程当然是官方入门教程:
https://tour.go-zh.org/

官方入门教程太简单(毕竟Go本身语法就是简单),还需要阅读其它相关知识:
1)Go搭建一个Web服务器
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/03.2.md
2)golang读取json配置文件
[https://blog.csdn.net/benben_2015/article/details/79134734]
3)文件读写
https://wiki.jikexueyuan.com/project/the-way-to-go/12.2.html
4)go-extend,获取请求的IP的代码
https://github.com/thinkeridea/go-extend/blob/master/exnet/ip.go
5)golang 发送GET和POST示例
https://segmentfault.com/a/1190000013262746
6)GoDNS中dnspod客户端的代码
https://github.com/TimothyYe/godns/blob/master/handler/dnspod/dnspod_handler.go
7)Go语言(golang)的错误(error)处理的推荐方案
https://www.flysnow.org/2019/01/01/golang-error-handle-suggestion.html

这个小服务,就是个web服务。客户端发起带有key和token的请求,此服务会验证有效的授权,然后把对应的二级域名与客户端IP绑定。配置信息以json格式保存在文本文件。客户端IP会记录在对应log文件,以方便每次比较客户端IP是否变化了。每次更新二级域名与IP的绑定,则会记录log。相关文件及代码如下:

配置文件,config.json:

{
"ServIpPort": ":12345"
,"DnsKey": "dnspod的key"
,"DnsToken": "dnspod的token"
,"DomainId": "dnspod的域名id"
,"SubDomainId": {"abc":"dnspod的二级域名id", "efg":"dnspod的二级域名id"}
,"Users": [
        {
                "Key":"client1"
                ,"Token":"aaa123456"
                ,"SubDomains": ["abc"]
        }
        ,{
                "Key":"client2"
                ,"Token":"xxx789012"
                ,"SubDomains": ["efg"]
        }
]
}

小服务的代码,ddnsServ.go:

package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "net/url"
    "strings"
    "os"
    "path/filepath"
    "io/ioutil"
    "time"
    "encoding/json"
)

type User struct {
    Key string
    Token string
    SubDomains []string
}

type Config struct {
    ServIpPort string
    DnsKey string
    DnsToken string
    DomainId string
    SubDomainId map[string]string /*key:sub domain name, value:sub domain id*/
    Users []User
}

var (
    curPath string
    ipLogPath string
    historyPath string
    config Config
)

func init() {
    curPath, _ = filepath.Abs(filepath.Dir(os.Args[0])) 
    ipLogPath = curPath + "/ip"
    historyPath = curPath + "/log"
    for _, path := range []string{ipLogPath, historyPath} {
        if err := initPath(path); err != nil {
            fmt.Printf("Init failed. Error info:%s\n", err)
            os.Exit(-1)
            return
        }
    }

    if err := initConfig(curPath + "/config.json"); err != nil {
        fmt.Printf("Init failed. Error info:%s\n", err)
        os.Exit(-1)
        return
    }
}

func initPath(path string) error {
    s, err := os.Stat(path)
    if err == nil && !s.IsDir() {
        return fmt.Errorf("The path is existed, but it is not a directory! Path is:%s", path)
    }
    if err != nil && os.IsNotExist(err) {
        e := os.Mkdir(path, os.ModePerm)
        return e 
    }
    return nil
}

func initConfig(configPath string) error {
    configData, err := ioutil.ReadFile(configPath)
    if err != nil {
        return fmt.Errorf("Failed to read config file: %s! Error info: \n%s", configPath, err)
    }

    err = json.Unmarshal(configData, &config)
    if err != nil {
        return fmt.Errorf("Failed to load config data! Error info: \n%s", err)
    }
    return nil
}

// get client IP address 
func GetClientIP(r *http.Request) string {
    xForwardedFor := r.Header.Get("X-Forwarded-For")
    ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
    if ip != "" {
        return ip
    }

    ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
    if ip != "" {
        return ip
    }

    if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
        return ip
    }

    return ""
}

func GetLogFilePath(logPath string, subDomain string) string {
    return fmt.Sprintf("%s/%s.log", logPath, subDomain)
}

// get the file path which saved ip address of subDomain
func GetIpLog(subDomain string) string {
    path := GetLogFilePath(ipLogPath, subDomain)
    buf, err := ioutil.ReadFile(path)
    if err != nil {
        return ""
    }
    return string(buf)
}

func SaveIpLog(subDomain string, ip string) {
    path := GetLogFilePath(ipLogPath, subDomain)
    ioutil.WriteFile(path, []byte(ip), 0644)
    /*
    err := ioutil.WriteFile(path, []byte(ip), 0644)
    if err != nil {
        panic(err.Error())
    }
    */
}

func SaveHistoryLog(subDomain string, ip string) error {
    path := GetLogFilePath(historyPath, subDomain)
    logFile, err := os.OpenFile(path, os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0644)
    if err != nil {
        return fmt.Errorf("Failed to open history log file: %s! Error info: \n%s", path, err)
    }
    defer logFile.Close()

    nowStr := time.Now().Format("2006-01-02 15:04:05")
    log := fmt.Sprintf("%s, ip:%s\n", nowStr, ip)
    logByte := []byte(log)
    n, err := logFile.Write(logByte)
    if err == nil && n < len(logByte) {
        return fmt.Errorf("Failed to save history log file: %s! Error info: \nwrite file failed", path)
    }
    return nil
}

func UpdateDns(subDomain string, ip string) error {
    values := url.Values{}
    values.Add("login_token", config.DnsKey + "," + config.DnsToken)
    values.Add("format", "json")
    values.Add("lang", "en")
    values.Add("error_on_empty", "no")

    values.Add("domain_id", config.DomainId)
    values.Add("record_id", config.SubDomainId[subDomain])
    values.Add("sub_domain", subDomain)
    values.Add("record_type", "A")
    values.Add("record_line", "默认")
    values.Add("value", ip)

    client := &http.Client{}
    req, err := http.NewRequest("POST", "https://dnsapi.cn/Record.Modify", strings.NewReader(values.Encode()))
    if err != nil {
        // handle error
    }

    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Set("Accept", "text/json")
    resp, err := client.Do(req)

    defer resp.Body.Close()

    _, err2 := ioutil.ReadAll(resp.Body)
//    fmt.Println(string(s))
    if err2 != nil {
        return err2
    }
    return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    // Verify authorization
    var subDomains []string
    key := r.PostFormValue("key") // get key of POST
    token := r.PostFormValue("token") // get token of POST
    for _, user := range config.Users {
        if user.Key == key && user.Token == token {
            subDomains = user.SubDomains
            break
        }
    }
    if subDomains == nil || len(subDomains) <= 0 {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    // get IP
    ip := GetClientIP(r)
    fmt.Fprintf(w, "%s", ip)

    for _, subDomain := range subDomains {
        // get last IP of subDomain
        ipLog := GetIpLog(subDomain) 
        if ip == ipLog {
            // IP is not changed
            continue
        }
    
        // update DNS, bind IP to subDomain
        err := UpdateDns(subDomain, ip)
        if err == nil {
            // update success, save new IP
            SaveIpLog(subDomain, ip)
            SaveHistoryLog(subDomain, ip)
        }
    }
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(config.ServIpPort, nil))
}

关于部署,就用nginx弄个反向代理,指向这个小服务的端口。由于VPS上装了Debian9,可以配置systemd来设置系统服务。

客户端只需发起post请求,把key和token发过来就可以了。以下用curl实现:

#!/bin/sh

#curl命令的参数解析:
# -X 请求的方法。这里用了POST,在HTTPS传输中,数据被加密
# --connect-timeout 连接超时时间,单位:秒
# -m/--max-time 数据传输的最大允许时间,单位:秒
# https://rpi.f...... 请求的URL
# -H/--header 请求头。要设置多个请求头,则设置多个-H参数
# -d/--data 请求数据。
curl -k -X POST --connect-timeout 5 -m 10 https://youdomain.xxx:12345/api/update_dns -H 'cache-control: no-cache' -H 'content-type: application/x-www-form-urlencoded' -d 'key=client1&token=aaa123456'

目前这个小服务工作良好。除了体积有点大(约7MB),其它都挺满意的。

近来迁移了自家的服务器,顺便记录一下Flask项目的部署。

这里采用Nginx + Supervisor + Python3 + uWSGI + Flask的方案。其中建议把uWSGI替换成Gunicorn,据说采用纯Python实现的Gunicorn,更方便打包为Dockor镜像,并且性能几乎跟Supervisor一样。这个留待以后再研(折)究(腾)。

新服务器采用Debian 9,部署过程参考以下文章:
Flask+uwsgi+Nginx部署应用
https://www.jianshu.com/p/84978157c785


1. 安装项目虚拟环境

假设项目文件夹为`/opt/flask_proj`,安装命令如下:

sudo apt update
sudo apt install python3 python3-pip
sudo pip3 install vertualenv
cd /opt/flask_proj
virtualenv venv
pip3 install -r requirements.txt

其中:
1)安装vertualenv需要用root用户,否则不会安装到/usr/local/bin/virtualenv,并且需要自行添加到path。
2)requirements.txt为flask项目所需的库。这个要看项目是否需要安装。
3)如果是迁移项目,可以在迁移前生成requirements.txt文件:pip3 freeze > requirements.txt


2. 安装uWSGI

如果requirements.txt里已包含uWSGI,则不用重复安装。安装命令如下:

pip3 install uwsgi

在项目文件夹下,新建uWSGI配置文件config.ini,参考内容如下:

[uwsgi]
master = true
home = venv
wsgi-file = manage.py
callable = app
socket = :5001
processes = 4
threads = 2
buffer-size = 32768

3. 安装supervisor

安装命令如下:

sudo apt-get install supervisor

在文件夹/etc/supervisor/conf.d下新建项目对应的配置文件,例如flask_proj.conf,参考内容如下:

[program:flask_proj]
# 启动命令入口
command=/opt/flask_proj/venv/bin/uwsgi /opt/flask_proj/config.ini
# 命令程序所在目录
directory=/opt/flask_proj
# 运行命令的用户名
user=user
autostart=true
autorestart=true
# 日志地址
stdout_logfile=/opt/flask_proj/logs/uwsgi_supervisor.log

启动supervisor服务:

sudo service supervisor start

4. 安装Nginx

一般安装系统自带的版本就够用了:

sudo apt install nginx

在文件夹/etc/nginx/sites-available下新建配置文件flask_proj,参考内容如下:

server {
    listen 443 ssl http2 default_server;
    #listen [::]:443 ssl;
    server_name www.abc.xyz;

    ssl_certificate /etc/letsencrypt/live/www.abc.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.abc.xyz/privkey.pem;

    index index.html index.htm;

    gzip            on;
    gzip_min_length 1k;
    gzip_buffers    4 16k;
    #gzip_proxied    expired no-cache no-store private auth;
    gzip_comp_level 5;
    gzip_types      text/html text/css text/javascript text/json text/plain text/xml application/javascript application/json application/soap+xml application/x-javascript application/x-www-form-urlencoded application/xhtml+xml application/xml;

    location / {
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:5001;
        uwsgi_param UWSGI_PYHOME /opt/flask_proj/venv;
        uwsgi_param UWSGI_CHDIR /opt/flask_proj;
        uwsgi_param UWSGI_SCRIPT manage:app;
        uwsgi_read_timeout 100;
    }  
}

注意:
1)这里只配置了https,相关的SSL证书,可以到https://letsencrypt.org/免费申请。
2)开启了gzip压缩。

最后添加链接文件,并重启Nginx:

sudo ln -s /etc/nginx/sites-available/flask_proj /etc/nginx/sites-enable/flask_proj
sudo service nginx restart

从G1时代开始,就了解到因为Android使用Linux内核,可以利用chroot运行大量Linux发行版。但是由于当时ARM CPU性能低下及内存不足,一般只能使用Terminal字符界面,或者ssh过去。然后升级过设备,并装上了“XServer XSDL”(Android上的Xserver)用来体验图形界面,但是手机屏幕太小,一弹出虚拟键盘就基本把桌面挡住了。后来想起X Window是基于客户端/服务器模式的,应该可以用PC电脑之类提供Xserver,显示手机上的Linux图形界面。最后终于弄明白了配置,出来的效果还是不错,至少可以用浏览器流畅播放视频了。

所需设备
S机,用于提供X Server的设备,最好是屏幕比较大的PC电脑(台式机或笔记本)。系统最好是Linux,装上X11。后来发现Windows也可以,因为有Xming。当然,Android也是可以,因为有XServer XSDL。这里只记录Linux的。

A机,Android设备(手机或平板),运行Linux发行版的X Client。Android上有很多装Linux的应用了,这里推荐Linux Deploy,因为这是github.com上的开源项目。装Linux的步骤不详述,这个应用已经做得很好了。

网络设置
只要两个设备在同一局域网内就可以了。下面列出几种方式:
1)使用有线/无线路由器组建局域网。路由器是性能瓶颈,特别是A机无线连接到路由。
2)A机分享无线网络,S机连过去。缺点是只能利用A机的移动网络上网,费钱……
3)S机分享无线网络,A机连过去。S机需要装个无线网卡,并成为性能瓶颈。
4)A机开启开发者模式,通过USB线连接S机。S机利用ADB命令的forward功能映射端口。ADB命令成为性能瓶颈。
5)A机分享有线网络,通过USB线连接S机。这个方案性能最佳,且使用设备最小,又不影响A机的网络。但A机可能会出现发热的情况。

X Server设置
这一步花了很多时间。理解后就是S机上的一个命令和一个配置文件,即如何启动X Server和设置验证。相关原理可查Google,这里设置X Server为:1.0(从0开始算,第2个服务),默认使用6001端口。
1)利用xhost设置可访问X Server的客户端IP。在S机上修改配置文件/etc/X1.hosts,若不存在则新建。把A机的IP地址填进去,并保存。
2)运行以下命令启动X Server:

sudo Xorg :1.0 -listen tcp

X Client设置
A机上需要装好桌面环境。推荐LXDE吧,轻量。启动前,连好网络,在Linux Deploy上设置:

图形界面 -> 勾选启用
图形子系统 -> 选X11
图形界面设置 -> 显示编号 -> 1.0
           -> X服务器地址 -> 填写S机的IP地址
           -> XServer XSDL -> 不要勾选
桌面环境 -> 选LXDE

然后启动Linux即可(实在太方便了)。对应的命令就是:

export DISPLAY=S机_IP:1.0
startlxde

总结
A机的性能越好,并且S机的性能越差,这个方案的实用性就越高。比起微软的“Continue on PC”,三星的DeX,Superbook等方案,便宜很多,并把旧电脑利用起来。使用过程中,不会影响A机的来电、通知等日常用途。但是目前来看,实用性确实不高。后面看看能不能用Raspberry Pi Zero + 显示屏 + 大容量移动电源来作为S机,以提高便携性。

CFO离职,捡了个烂电脑回来(CPU是i3-4000M,第4代酷睿,内存4G)。比10年前的酷睿T2450好太多了,而且是64位系统。前段时间有新闻报道说,Google的内部系统Goobuntu,准备抛弃Ubuntu,直接基于Debian。于是也想试试直接装Debian来用。其实用习惯了Lubuntu,只要桌面也装LXDE,应该用起来没什么区别。
第一次装Debian,发现居然连下载哪个ISO都不知道。一开始下个live CD的iso文件,发 现虽然启动界面有选项可以安装系统,但是按抓给你过程要检查CD-ROM。没用光盘的话,不能继续进行安装。后来才明白要用netinst的iso。

iso文件制作U盘启动盘很简单,只要两行命令。其中sdX是U盘的设备文件名。如果U盘已挂载,要先卸载,再执行

$ sudo cp debian-9.4.0-amd64-netinst.iso /dev/sdX
$ sudo sync

后面的安装步骤基本按着提示一步一步进行。有个文章写得挺好的,可以参考:
如何安装 Debian 9?
http://scottming.com/2017/08/06/how_to_install_debian9/

有两个步骤需要说明一下:
第一,需要找无线网卡的firmware,放在另一个U盘,再插到电脑上。安装程序会自动设别并安装。这是为了后面安装软件时连上网络。或者可以使用有线连接吧。

第二,到了分区那一步,不能按默认的整个硬盘分区。因为要保留原来的Windows系统,所以只能选择手动操作。而且第一次搞LVM分区,完全不懂操作。后来参考了这个文章才能顺利搞掂:
ubuntu 12.10 安装 LVM分区(图文)
http://blog.sina.com.cn/s/blog_56a70c0401018dki.html

简单来说,要注意几点:
1)由于GRUB Legacy不支持LVM,所以无法在LVM上创建/boot分区。也就是除了/boot分区,其它分区都可以在LVM上创建。
2)先把分区设置为LVM分区,再点LVM分区管理的选项进行详细设置。
3)LVM分区管理,先创建逻辑卷组,再创建逻辑卷,多个逻辑卷就等于是多个分区。
4)最后把逻辑卷设置对应的挂载点。

最后总算是顺利安装完毕。

后记
用了一段时间后,终于明白Ubuntu的存在价值了。Debian 9即使装上LXDE,还是有一些不顺手,例如屏幕亮度调节。后来还是换上Lubuntu。

手上有个远古时代的上网本,华硕(ASUS) EeePC 900。删掉原来的Windows XP,装上Lubuntu 16.10。原电池废了,淘宝换了个新的,续航能达到5小时(大概吧)。但是一般用来远程ssh到raspberry pi,没必要开启图形界面(特别是发现用screen可以同一界面打开多个shell)。需要解决的问题是中文的显示和输入。Google过后,最终的解决方案是fbterm(完美显示中文) + fcitx-fbterm(实现中文输入)。

fbterm的安装参考这个(英文):
https://fcitx-im.org/wiki/Fbterm

中文版本的完整安装教程,参考这个:
http://ikuduku.com/blog/fbterm-display-and-input-Chinese-in-tty

关于中文输入法,我选择google拼音(fcitx-googlepinyin)。记录一下相关操作:

1)安装

#安装相关软件
sudo apt-get install fbterm fcitx fcitx-fbterm fcitx-googlepinyin fcitx-configtool
#让fbterm可以设置快捷键,主要是切换输入法
sudo setcap 'cap_sys_tty_config+ep' /usr/bin/fbterm
#把当前用户加入到video组
sudo gpasswd -a <your username> video

2)编辑当前用户的配置

vi ~/.fbtermrc

修改如下:

#设置字体
font-name=Mono
#设置字体大小
font-size=14
#设置输入法
input-method=fcitx-fbterm

3)设置开机启动

#设置开机进入shell界面,这是Ubuntu 16.04及以后版本的设置方法
sudo systemctl set-default multi-user.target
#设置进入图形界面的命令,然后用`sudo startx`命令即可进入图形界面
sudo sh -c "echo \#\!/bin/sh > /usr/local/bin/startx"
sudo sh -c "echo systemctl start lightdm >> /usr/local/bin/startx"
sudo chmod +x /usr/local/bin/startx
#设置进入shell后自动启动fbterm,而图形界面的shell则不会启动fbterm
printf '\nif [ $TERM = 'linux' ]; then\n  fcitx-fbterm-helper -l\nfi' >> ~/.bashrc

大概就这样了。使用过程中,还是有一点不习惯。例如快捷键的变换,Ctrl + Alt + F1 ~ F6 不能切换tty,而是切换字符编码,使用 Ctrl + Alt + 1 ~ 0 可以切换10个窗口。

一般一个月一次整理孩子的照片。但是老婆的手机和单反拍出来的照片,文件名都不统一。要统一采用拍照时间做为文件名的话,比较麻烦。本来想用Python3写个脚本处理的,但是官方没有提供获取EXIF信息的方法。找打其它包,也不怎么好用。但是今晚发现原来有个shell命令挺好用的,就是 EXIF。

于是就写了个shell脚本,如下:

#!/bin/bash

path=/home/fox/pics

i=$(ls $path/*.JPG)

for j in $i
do
  d=$(exif -t "DateTime" $j | grep Value)
  d=${d/  Value: /IMG_}
  d=${d//:/}
  d=${d// /_}
  $(mv $j ${d}.JPG)
done

这里采用的统一文件名格式是IMG_yyyyMMdd_HHmmss.jpg。由于采用字符串替换的语法,所以只能用bash运行。

某日发现了提供免费SSL证书的网站——www.sslforfree.com,立马手动申请了个证书来玩。部署以后,非常感动,重点是Chrome和FireFox都支持!曾在某知名网站申请过免费证书,总是申请失败。心灰意冷,本来准备买个廉价的付费版,幸好发现这网站!

免费虽好,但有效期只有3个月,而且到期后只能手动更新证书。不过找到提供该免费证书的网站——Let’s Encrypt(https://letsencrypt.org),以及该网站提供的自动更新证书的开源软件certbot(https://certbot.eff.org)。certbot首页就有安装教程,对应不同HTTP服务和操作系统。安装完毕后,执行以下命令,就可以获取证书了

#验证域名并获取SSL证书
sudo certbot certonly --webroot -w /var/www/example -d example.com -d www.example.com

最后,设置个自动更新,就安枕无忧了。

PS.既然有了免费的证书,何不部署个HTTP 2.0?

上星期终于搞掂了办公室的CentOS备份。一开始,我们把硬盘A完全复制到硬盘B(用dd命令拷贝整个硬盘),产生了很多误会,也搞出不少问题,差点以为把硬盘A的数据搞丢了。

后来才发现,CentOS挂载硬盘分区时,根据分区的UUID来挂载的。由于两个硬盘的数据完全一致(包括各分区的UUID也一致),所以即使以硬盘A启动电脑,也可能挂载了硬盘B的分区。最后将错就错,按如下步骤解决问题:

1)修改硬盘B中所有分区的UUID
参考以下文章:
linux下硬盘uuid查看及修改设置
http://blog.csdn.net/rainday0310/article/details/6343038

Ext4的分区可以按照该文章,修改分区UUID:

uuidgen | xargs tune2fs /dev/sdb1 -U

但是swap分区不行,只能格式化一下,让其重新生成UUID:

#操作之前,先取消挂载
swapoff /dev/sdb2
#格式化swap分区
mkswap /dev/sdb2

修改后,可以下命令查看各分区的UUID:

blkid /dev/sdb1

2)修改硬盘B上的相关配置文件
此步骤是为了让硬盘B也能直接启动。就是说硬盘A出了什么故障,直接用硬盘B就可以启动服务器了。
a)修改分区挂载文件/etc/fstab,把对应分区的UUID改为新的。
b)修改grub配置文件/boot/grub/grub.conf,把根目录的分区UUID改为最新的。

3)同步两个硬盘的文件
这里采用rsync命令,把硬盘A的数据自动同步到硬盘B上。
a)把硬盘B的根目录挂载到/media/sdb3
b)创建文件/media/rsync_exclude文件,把要排除的文件夹或文件录进去,一行一个,参考内容如下:

/boot/*
/dev/*
/media/*
/lost+found
/proc/*
/sys/*
/tmp/*
/etc/fstab

c)执行以下命令,立即同步文件(以root用户执行):

rsync -avzP --delete --exclude-form=/media/rsync_exclude / /media/sdb3 >> /media/rsync.log

参考:
rsync官方man文档
https://download.samba.org/pub/rsync/rsync.html

CentOS 6.5下rsync服务器安装配置,rsync 客户端
https://segmentfault.com/a/1190000002502991#articleHeader5

4)优化
后面考虑把硬盘B做成USB移动硬盘,这样可以在需要时才接上电源。再考虑接上硬盘B后自动执行同步命令。