手工编译安装 macOS 下的 Rime(鼠须管)

因为 macOS 下的 Rime 输入法(鼠须管) 不是经常更新二进制,所以要体验性特性总是要手工进行编译安装。

之前的想要 Rime 实现按下 Esc 切换为英文时,看到 commit history 有提交的时候就尝试手工编译安装了一下。一直都在笔记里面,现在整理一下发出来。

Prerequisites

安装 Xcode 12.2 及以上

首先从 App Store 中安装 Xcode 12.2 及以上版本。

如果只有 Xcode 10 只能编译 x86_64 的版本。

安装 cmake

官网 下载安装。

或者从Homebrew 安装:

brew install cmake

或者从 MacPorts 安装:

port install cmake

Checkout the code

获取 Squirrel 的源码:

git clone --recursive https://github.com/rime/squirrel.git

cd squirrel

通过如下方式获取 Rime 的插件(这一步如果不需要可以跳过,不过建议安装特定的插件以提高使用舒适度):

bash librime/install-plugins.sh rime/librime-sample # ...
bash librime/install-plugins.sh hchunhui/librime-lua
bash librime/install-plugins.sh lotem/librime-octagram

添加两个插件 librime-lua,librime-octagram

  • librime-lua 可以使用户可以编写 lua 脚本,编写函数来处理输出,比如在英文单词后面自动添加一个空格,或者当输入 date 或 「日期」的时候自动出现当前的日期。

  • librime-octagram 是八股文插件,通过提前训练的模型增强 RIME 的长句组词能力

Shortcut: get the latest librime release

You have the option to skip the following two sections - building Boost and librime, by downloading the latest librime binary from GitHub releases.

可以直接执行如下命令从 GitHub release 页面下载编译好的 Boost 和 librime,跳过下面两个步骤:

bash ./travis-install.sh

准备工作做好之后,就可以开始编译 Squirrel

Install Boost C++ libraries

选择下面两种方式中的一个安装 Boost 库。

Option: 下载源码编译安装:.

export BUILD_UNIVERSAL=1

make -C librime xcode/thirdparty/boost

export BOOST_ROOT="$(pwd)/librime/thirdparty/src/boost_1_75_0"

Let’s set BUILD_UNIVERSAL to tell make that we are building Boost as universal macOS binaries. Skip this if building only for the native architecture.

After Boost source code is downloaded and a few compiled libraries are built, be sure to set shell variable BOOST_ROOT to its top level directory as above.

You may also set BOOST_ROOT to an existing Boost source tree before this step.

Option: 从 Homebrew 从安装:

brew install boost

Note: with this option, the built Squirrel.app is not portable because it links to locally installed libraries from Homebrew.

Learn more about the implications of this at https://github.com/rime/librime/blob/master/README-mac.md#install-boost-c-libraries

Option:MacPorts 安装:

port install boost -no_static

Build dependencies

Again, set BUILD_UNIVERSAL to tell make that we are building librime as universal macOS binaries. Skip this if building only for the native architecture.

Build librime, dependent third-party libraries and data files:

export BUILD_UNIVERSAL=1

make deps

Build Squirrel

当所有的依赖都安装准备好之后, 开始编译 Squirrel.app:

make

To build only for the native architecture, pass variable ARCHS to make:

# for Mac computers with Apple Silicon
make ARCHS='arm64'

# for Intel-based Mac
make ARCHS='x86_64'

Install it on your Mac

编译后之后就可以安装到系统上:

# Squirrel as a Universal app
make install

# for Intel-based Mac only
make ARCHS='x86_64' install

之后就可以享受完美的 Rime 输入法体验了。

Question

make ARCHS='x86_64' 的时候遇到错误:

CompileXIB /Users/einverne/Git/squirrel/zh-Hans.lproj/MainMenu.xib (in target 'Squirrel' from project 'Squirrel')
    cd /Users/einverne/Git/squirrel
    export XCODE_DEVELOPER_USR_PATH\=/Applications/Xcode.app/Contents/Developer/usr/bin/..
    /Applications/Xcode.app/Contents/Developer/usr/bin/ibtool --errors --warnings --notices --module Squirrel --output-partial-info-plist /Users/einverne/Git/squirrel/build/Squirrel.build/Release/Squirrel.build/zh-Hans.lproj/MainMenu-PartialInfo.plist --auto-activate-custom-fonts --target-device mac --minimum-deployment-target 10.9 --output-format human-readable-text --compile /Users/einverne/Git/squirrel/build/Release/Squirrel.app/Contents/Resources/zh-Hans.lproj/MainMenu.nib /Users/einverne/Git/squirrel/zh-Hans.lproj/MainMenu.xib
Command CompileXIB failed with a nonzero exit code

** BUILD FAILED **


The following build commands failed:
        CompileXIB /Users/einverne/Git/squirrel/Base.lproj/MainMenu.xib (in target 'Squirrel' from project 'Squirrel')
        CompileXIB /Users/einverne/Git/squirrel/zh-Hant.lproj/MainMenu.xib (in target 'Squirrel' from project 'Squirrel')
        CompileXIB /Users/einverne/Git/squirrel/zh-Hans.lproj/MainMenu.xib (in target 'Squirrel' from project 'Squirrel')
(3 failures)
make: *** [release] Error 65
  1. removing the old tools ($ sudo rm -rf /Library/Developer/CommandLineTools)
  2. install xcode command line tools again ($ xcode-select --install).
ld: warning: directory not found for option '-L/usr/local/lib/Release'
ld: warning: directory not found for option '-L/Users/einverne/Git/squirrel/librime/thirdparty/lib/Release'
ld: library not found for -licudata
clang: error: linker command failed with exit code 1 (use -v to see invocation)

2021-07-11 rime , squirrel , macos , input-method , mac

手工编译安装 librime

librime 是 Rime,包括各个系统上的桌面版,Squirrel(鼠须管) 等等依赖的核心库。

Preparation

首先要安装 Xcode 和命令行工具,以及必要的编译工具:

brew install cmake git

Get the code

获取代码:

git clone --recursive https://github.com/rime/librime.git

or download from GitHub, then get code for third party dependencies separately.

Install Boost C++ libraries

安装 Boost 库,Boost 库是一个 C++ 的第三方库,Rime 大量地依赖了这个库。

选择一 (推荐): 下载源码,手工编译:

cd librime
make xcode/thirdparty/boost

The make script will download Boost source tarball, extract it to librime/thirdparty/src/boost_<version> and create needed static libraries for building macOS uinversal binary.

Set shell variable BOOST_ROOT to the path to boost_<version> directory prior to building librime.

export BOOST_ROOT="$(pwd)/thirdparty/src/boost_1_75_0"

选择 2: 从 Homebrew 安装 Boost 库:

brew install boost

如果你只想编译,并且安装到自己的 macOS 上,这是一个节省时间的选择。通过 Homebrew 中的 Boost 编译安装的 librime 可能不能在其他机器上完美的工作。

Built with Homebrewed version of Boost, the librime binary will not be portable to machines without certain Homebrew formulae installed.

选择 3: Install an older version of Boost libraries from Homebrew.

Starting from version 1.68, boost::locale library from Homebrew depends on icu4c, which is not provided by macOS.

Make target xcode/release-with-icu tells cmake to link to ICU libraries installed locally with Homebrew. This is only required if building with the librime-charcode plugin.

To make a portable build with this plugin, install an earlier version of boost that wasn’t dependent on icu4c:

brew install boost@1.60
brew link --force boost@1.60

Build third-party libraries

Required third-party libraries other than Boost are included as git submodules:

# cd librime

# if you haven't checked out the submodules with git clone --recursive ..., do:
# git submodule update --init

make xcode/thirdparty

This builds libraries located at thirdparty/src/*, and installs the build artifacts to thirdparty/include, thirdparty/lib and thirdparty/bin.

You can also build an individual library, eg. opencc, with:

make xcode/thirdparty/opencc

Build librime

make xcode

This creates build/lib/Release/librime*.dylib and command line tools build/bin/Release/rime_*.

Or, create a debug build:

make xcode/debug

Run unit tests

make xcode/test

Or, test the debug build:

make xcode/test-debug

Try it in the console

(
  cd debug/bin;
  echo "congmingdeRime{space}shurufa" | Debug/rime_api_console
)

Use it as REPL, quit with Control+d:

(cd debug/bin; ./Debug/rime_api_console)

2021-07-02 rime , squirrel , input-method , macos , mac , open-source

JWT 认证使用

现代 Web 应用一般常用的认证方式有如下两种:

  • session
  • cookie

session 认证需要服务端大量的逻辑处理,保证 session 一致性,并且需要花费一定的空间实现 session 的存储。

所以现代的 Web 应用倾向于使用客户端认证,在浏览器中就是 cookie 认证, 但是 Cookie 有明显的缺陷:

  • Cookie 会有数量和长度限制
  • Cookie 如果被拦截可能存在安全性问题

为什么要认证

数据安全:

  • 进行安全的验证,服务端可以无状态认证

签名,只有信息发送者才能产生别人无法伪造的字串,这个字串同时是发送者真实信息的证明。

用户登录成功后,服务端产生 token 字串,并将字串下发客户端,客户端在之后的请求中携带 token。

Token 验证的优点

  • 支持跨域访问,Cookie 不允许跨域访问
  • 无状态,服务端不需要存储 session 信息,Token 自身包含了所有登录用户的信息
  • 解耦,不需要绑定到一个特定的身份验证方案,Token 可以在任何地方生成
  • 适用范围广,只要支持 HTTP 协议客户端就可以使用 Token 认证
  • 服务端只需要验证 Token 安全,不必再获取登录用户信息
  • 标准化,API 可以采用标准化的 JWT(JSON Web Token)

Token 的缺点

  • 数据传输量大,Token 存储了用户相关的信息,比单纯的 Cookie 信息要多,传输过程中消耗更多的流量
  • 和所有客户端认证方式一样,很难在服务端注销 Token,很难解决客户端劫持问题。并且 Token 一旦签发了,在到期之前就始终有效,除非服务器部署额外的逻辑
  • Token 信息在服务端增加了一次验证数据完整性的操作,比 Session 认证方式增加了 CPU 开销

JWT

JSON Web Token(JWT) 是一个开放标准(RFC 7519),定义了紧凑、自包含的方式,用于 JSON 对象在各方之间传输。

JWT 实际就是一个字符串,三部分组成:

  • 头部
  • 载荷
  • 签名

Header 由两部分组成,token 类型和算法名称(HMAC SHA256,RSA 等等)

{
  "alg": "HS256",
  "typ": "JWT"
}

payload

Payload 部分也是 JSON 对象,存放实际传输的数据,JWT 定义了7个官方的字段。

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

可以添加任何字段:

{
    "Name":"Ein Verne",
    "Age":18
}

Signature

签名部分通过如下内容生成:

  • 编码过的 header
  • 编码过的 payload
  • 一个密钥(只有服务端知道)

通过指定的签名算法加密:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

算出签名之后,header, payload, signature 三个部分拼成字符串,用 . 分隔,返回给用户。

Token 认证流程

  1. 客户端携带用户登录信息(用户名、密码)提交请求
  2. 服务端收到请求,验证登录信息,如果正确,则按照协议规定生成 Token,经过签名并返回给客户端
  3. 客户端收到 Token,保存在 Cookie 或其他地方,每次请求时都携带 Token
  4. 业务服务器收到请求,验证 Token 正确性

无论是 Token,Cookie 还是 Session 认证,一旦拿到客户端标识,都可以伪造。为了安全,任何一种认证方式都要考虑加入来源 IP 或白名单,过期时间。

JWT 如何保证安全性

JWT 安全性保证的关键就是 HMACSHA256,等等加密算法,该加密过程不可逆,无法从客户端的 Token 中解出密钥信息,所以可以认为 Token 是安全的,继而可以认为客户端调用是发送过来的 Token 是可信任的。

常用的 Python 库

pyjwt

常用的 Java 库

jjwt

auth0

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

用法:

public static String create(){
  try {
    Algorithm algorithm = Algorithm.HMAC256("secret");
    String token = JWT.create()
      .withIssuer("auth0")
      .withSubject("subject")
      .withClaim("name","古时的风筝")
      .withClaim("introduce","英俊潇洒")
      .sign(algorithm);
    System.out.println(token);
    return token;
  } catch (JWTCreationException exception){
    //Invalid Signing configuration / Couldn't convert Claims.
    throw exception;
  }
}

验证 Token

public static Boolean verify(String token){
  try {
    Algorithm algorithm = Algorithm.HMAC256("secret");
    JWTVerifier verifier = JWT.require(algorithm)
      .withIssuer("auth0")
      .build(); //Reusable verifier instance
    DecodedJWT jwt = verifier.verify(token);
    String payload = jwt.getPayload();
    String name = jwt.getClaim("name").asString();
    String introduce = jwt.getClaim("introduce").asString();
    System.out.println(payload);
    System.out.println(name);
    System.out.println(introduce);
    return true;
  } catch (JWTVerificationException exception){
    //Invalid signature/claims
    return false;
  }
}

相关工具

reference


2021-06-29 jwt , authentication , session , cookie , python , java

Duplicacy 增量备份工具使用

Duplicacy 是一个用 Go 语言实现的,开源的,跨平台的备份工具。

特性:

  • 命令行版本对个人用户完全免费
  • 付费授权会提供了一个网页端管理
  • 支持 Amazon S3,Google Cloud Storage,Microsoft Azure,Dropbox 和 Backblaze 等云存储,本地磁盘,SFTP 等等
  • 支持多个客户端备份到同一个云存储
  • 支持增量备份
  • 支持加密备份

Lock Free Deduplication

这是一个对 Duplicacy 实现原理的简单介绍,完整的说明可以参考发布在 IEEE Transactions on Cloud Computing 的 Paper

Lock-Free Deduplication 的三个重要内容:

  • 使用 variable-size chunking 算法将文件分割成多块
  • 将每一块内容存储到云端空间,每一块的名字是其 hash,依赖文件系统的 API 来管理块,而不是用一个中心化的索引数据来管理
  • 当备份被删除时使用 two-step fossil collection 算法移除未被引用的块

variable-size chunking 算法又被称为 Content-Defined Chunking,被很多备份工具使用。相较于固定大小的块划分算法(rsync 所使用的),

检查一个块是否被上传过,只需要通过文件名(hash)执行一个文件查询。这使得只提供了非常有限操作的云端存储变成了一个非常强大的现代化的备份工具后端,既可以实现 block-level 的重复数据删除,也可以实现 file-level 的重复数据删除。不依赖于一个中心的索引数据库也就意味着没有必要实现一个存储系统上的分布式锁。

通过消除 chunk indexing database, 无锁的备份不仅减少了代码复杂度,也使得删除重复的过程变得没那么容易出错。

但存在一个问题,当并发访问时,如果没有一个中心化的索引数据库,那么删除 snapshots 的操作就变得非常困难。单一节点去访问文件存储是可以保证的,但是删除的操作可以简化成搜索没有被任何备份引用的块,然后删除他们。但是如果并发的访问,那么一个未被引用的块就不能被轻易地移除,因为可能另外一个备份进程正在引用同一块。正在执行的备份程序可能并不知道删除进程,所以在扫描的过程中可能认为该块存在,而不上传该块,但是删除进程可能在此时删除了该块,那么就造成了数据丢失。

但幸运的是,Lock-free deduplication 的删除问题有一个解决方案,就是 two-step fossil collection algorithm。这个算法会使用两个步骤来删除未被引用的块:

  • identify, collect them in the first step
  • permanently remove them once certain conditions are met

安装

从项目 release 页面下载可执行二进制文件。

sudo wget -O /opt/duplicacy https://github.com/gilbertchen/duplicacy/releases/download/v2.0.10/duplicacy_linux_x64_2.0.10
sudo ln -s /opt/duplicacy /usr/local/bin/duplicacy
sudo chmod +x /usr/local/bin/duplicacy

macOS 下

https://github.com/gilbertchen/duplicacy/releases/download/v3.1.0/duplicacy_osx_arm64_3.1.0

前提知识

storage

在 Duplicacy 的概念中 storage 指的是备份存储的地方。这个地方可以是本地,也可以是 [[SFTP]],或者现成的云端存储服务比如 [[Backblaze]]。

repository

repository 可以理解成仓库,可以将一个本地文件夹作为仓库。

snapshot

snapshot 直译是快照,duplicacy backup 命令会将 repository 的一份本地快照备份到 storage。

使用

Duplicacy 相关的命令:

NAME:
   duplicacy - A new generation cloud backup tool based on lock-free deduplication

USAGE:
   duplicacy [global options] command [command options] [arguments...]

VERSION:
   2.7.2 (175ADB)

COMMANDS:
   init		Initialize the storage if necessary and the current directory as the repository
   backup	Save a snapshot of the repository to the storage
   restore	Restore the repository to a previously saved snapshot
   list		List snapshots
   check	Check the integrity of snapshots
   cat		Print to stdout the specified file, or the snapshot content if no file is specified
   diff		Compare two snapshots or two revisions of a file
   history	Show the history of a file
   prune	Prune snapshots by revision, tag, or retention policy
   password	Change the storage password
   add		Add an additional storage to be used for the existing repository
   set		Change the options for the default or specified storage
   copy		Copy snapshots between compatible storages
   info		Show the information about the specified storage
   benchmark	Run a set of benchmarks to test download and upload speeds
   help, h	Shows a list of commands or help for one command

GLOBAL OPTIONS:
   -verbose, -v 		show more detailed information
   -debug, -d 			show even more detailed information, useful for debugging
   -log 			enable log-style output
   -stack 			print the stack trace when an error occurs
   -no-script 			do not run script before or after command execution
   -background 			read passwords, tokens, or keys only from keychain/keyring or env
   -profile <address:port> 	enable the profiling tool and listen on the specified address:port
   -comment  			add a comment to identify the process
   -suppress, -s <id> [+]	suppress logs with the specified id
   -help, -h 			show help

初始化存储

Duplicacy 可以备份目录级别数据。

cd path/to/dir
duplicacy init mywork sftp://user@192.168.2.100/path/to/storage/
  • mywork 是 duplicacy 用来区分备份的 snapshot_id,用来区分不用存储库的标签
  • 远程的文件夹需要提前创建,duplicacy 不会自动创建文件。

开始备份

duplicacy backup -stats

每一次的备份都通过唯一的 repository id 和从 1 开始自增的 revision number 组成

备份

#默认命令
duplicacy backup
#如果有多个存储目标,可以用-storage指定存储名称
duplicacy backup -storage storage_name

查看快照

duplicacy list
# 查看指定存储的快照
duplicacy list -storage storage_name
# 查看所有存储的快照
duplicacy list -a

还原

可以使用如下的命令还原:

duplicacy restore -r revision_number

说明:

  • 这里的 revision_number 可以通过 list 命令查看。

删除历史快照

# 删除指定存储内所有快照
duplicacy prune -a
# 删除版本 2,`-r` 可以使用多次
duplicacy prune -r 2
# 删除一个范围
duplicacy prune -r 10-20

使用 -keep 选项可以指定保存策略,比如

duplicacy prune -keep 1:7

表示的是对于超过 7 天的版本,每天保留一个版本。总结一下,-keep 接受两个数字 n:m ,表示的是对于 m 天前的版本,每隔 n 天保留一个版本。如果 n 为 0,任何超过 m 天的版本会被删掉。

这样如果要实现删除 180 天前的版本:

duplicacy prune -keep 0:180

-keep 选项也可以使用多次,但是需要按照 m 值从大到小排列:

duplicacy prune -keep 0:180 -keep 7:30 -keep 1:7

备份到 Backblaze

[[Backblaze B2 Cloud Storage]] 提供了 10GB 免费存储空间

# 将本地存储加密备份到 B2 存储的 Bucket
duplicacy init -e repository_id b2://unique-bucket-name

执行命令后会需要输入 Backblaze 的 KeyID 和 applicationKey,这个在 Backblaze B2 后台可以查看。

执行备份:

duplicacy bacup

备份到多个存储

根据 [[3-2-1 备份原则]] 至少需要有三份完整的数据,其中一份必须在异地,Duplicacy 只需要添加多个存储即可实现多地备份。

cd path/to/dir
duplicacy init my-backups --storage-name backblaze b2://bucket-name
# add an additional storage
duplicacy add local snapshot_id /mnt/storage/
duplicacy add offsite_storage_name repository_id offsite_storage_url

说明:

  • add 子命令和 init 命令相差不多,主要区别在于需要为新的存储指定一个名字。这里的 offsite_storage_name 可以是任何想要的名字,主要是为了助记。Duplicacy 的第一个存储空间默认的名字是 default。

当配置完成后使用 duplicacy backup 即可以实现多处备份。

如果只想要备份到一个地方,也可以使用 -storage 指定:

duplicacy backup -storage offsite_storage_name

另外一种推荐的做法是使用 copy 命令,将默认的存储内容复制到新配置的存储(offsite_storage) 上:

duplicacy copy -from default -to offsite_storage_name

恢复到另外的文件夹或恢复到另外的电脑

cd path/to/dir1
duplicacy init backup1 sftp://user@192.168.2.100/path/to/storage
duplicacy backup -stats

如果在当前的文件夹想要恢复,那么直接使用 duplicacy restore -r 1 即可。

但是如果要在另外的文件夹,或另一台机器上恢复呢,也非常简单

cd path/to/dir2
duplicacy init backup1 sftp://user@192.168.2.100/path/to/storage
duplicacy restore -r 1

这里需要注意备份的远端地址需要是一样的。比如上面的例子中都使用 SFTP 的地址。

Duplicacy 支持的 Storage

Local

本地文件的话,直接写文件路径:

/path/to/backup

SFTP

SFTP 语法:

sftp://username@server

Dropbox

Storage URL:

dropbox://path/to/storage

Amazon S3

s3://amazon.com/bucket/path/to/storage (default region is us-east-1)
s3://region@amazon.com/bucket/path/to/storage (other regions must be specified)

需要提供 access key 和 secret key.

支持 [[2021-07-24-minio-usage MinIO 自建对象存储]]:
minio://region@host/bucket/path/to/storage (without TLS)
minios://region@host/bucket/path/to/storage (with TLS)

其他 S3 兼容的存储:

s3c://region@host/bucket/path/to/storage

Wasabi

wasabi://region@s3.wasabisys.com/bucket/path
wasabi://us-east-1@s3.wasabisys.com/bucket/path
wasabi://us-east-2@s3.us-east-2.wasabisys.com/bucket/path
wasabi://us-west-1@s3.us-west-1.wasabisys.com/bucket/path
wasabi://eu-central-1@s3.eu-central-1.wasabisys.com/bucket/path

DigitalOcean Spaces

s3://nyc3@nyc3.digitaloceanspaces.com/bucket/path/to/storage

Google Cloud Storage

gcs://bucket/path/to/storage

Google Cloud Storage 也可以在设置中开启 S3 兼容 那就可以使用:

s3://storage.googleapis.com/bucket/path/to/storage

Microsoft Azure

azure://account/container

NetApp StorageGRID

s3://us-east-1@storagegrid.netapp.com/bucket/path/to/storage

Backblaze B2

b2://bucketname

Google Drive

gcd://path/to/storage (for My Drive)
gcd://shareddrive@path/to/storage (for Shared Drive)

Microsoft OneDrive

one://path/to/storage (for OneDrive Personal)
odb://path/to/storage (for OneDrive Business)

Hubic

hubic://path/to/storage

OpenStack Swift

swift://user@auth_url/container/path

WebDAV

[[WebDAV]] 链接:

webdav://username@server/path/to/storage (path relative to the home directory)
webdav://username@server//path/to/storage (absolute path with double `//`)

更多的 Storage URL 可以参考这里

备份脚本

#!/bin/bash

#关闭服务

#duplicacy变量
export DUPLICACY_B2_ID=b2_id
export DUPLICACY_B2_KEY=b2_key
export DUPLICACY_PASSWORD=your_password

#导出数据库
cd /data/mysql/
/usr/local/mysql/bin/mysqldump -h 127.0.0.1 -P 3306 --all-databases > /data/mysql/all.sql
zip -mqP password ./all.sql.zip ./all.sql

#增量备份mysql文件夹
cd /data/mysql/
duplicacy backup

#增量备份wordpress文件夹
cd /home/wordpress/
duplicacy backup

#删除60天前的快照,超过30天的快照每15天保留一个
duplicacy prune -all -keep 0:60 -keep 15:30

#启动服务

Duplicacy vs duplicity

Duplicacy 和 duplicity 相比较而言,Duplicacy 在备份很多次的情况下会比 duplicity 占用更多的空间,但是 Duplicacy 每一次备份的时间都要远远少于 duplicity。

duplicity 有一个严重的缺陷在于其增量备份方法,每一次备份都需要用户选择是否全量备份或者增量备份,并且其设计决定了在一个备份了很多次的仓库中删除任何一个历史的备份变得不可能。

具体的比较可以参考这里

reference


2021-06-17 backup , backup-tool , duplicacy , backblaze , google-drive , rsync

使用 Netdata Cloud 监控所有的机器

很早就开始用 Netdata,新买来的 VPS 直接一行命令就可以安装,并且提供了一个非常不错的监控后台。但是因为没有办法在一个中心化的地方管理我所有的机器,所以之前都是用一个简单的 nodequery 服务来监控服务器是否在线,CPU、内存、流量使用率,但 nodequery 已经很多年没有更新,而最近去看 Netdata 官网的时候发现其退出了一个 Netdata Cloud 的服务,体验下来确实直接可以代替 nodequery 了。

什么是 Netdata Cloud

[[Netdata]] 是一款非常漂亮并且非常强大的监控面板,由于 Netdata 并没有提供验证等等功能,所以一旦启动,所有人都可以通过 IP:19999 来访问监控面板,虽然 Netdata 做了充分的安全检查,后台面板对系统只读,黑客或破坏分子并不能通过监控面板来控制系统,但是有心人还是能够通过面板来看出系统运行的服务,从而进行破坏,所以一般会通过反向代理或放在防火墙后面来规避安全问题,但与此同时带来的管理上的困难。

在之前 Netdata 没有提供 Netdata Cloud 服务之前,需要自己配置防火墙,只允许特定 IP 访问;或者配置反向代理,通过密码进行保护1。现在通过 Netdata Cloud 多了一种完美的解决方案,我们可以将 Netdata 数据添加到 Cloud,然后禁用本地暴露在 19999 端口的面板。

并且通过 Cloud 后台,可以在一个中心化的地方监控到所有机器的状况,并且 Netdata Cloud 还提供了免费的邮件报警服务。

安全性

  • Netdata 提供了他们的数据隐私政策
  • 所有的数据在传输过程中都通过 TLS 加密过

基础概念

虽然 Netdata Cloud 服务并不复杂,但这里还是要提前把一些概念理清楚一下。

Node

每一台机器都都相当于一个节点。

War Rooms

War Rooms 组织节点,提供了跨节点的视图。可以认为 War Rooms 是一系列节点的合集。

几种方式管理 War Rooms:

  • 根据服务(Nginx,MySQL,Pulsar 等等),目的(webserver, database, application),地点(服务器真实地址)等等来管理,比如可以根据
  • 将整个后端基础架构放到 War Rooms 管理,可以是 Kubernetes cluster, proxies, databases, web servers, brokers 等等

Spaces

Spaces 是一个高层级的抽象,用来管理团队成员和节点。Spaces 下面会有不同的 War Rooms。这也就意味着通过 Space 可以让成员和节点在一起。

这样也就可以将特定的后台分配给不同的成员。

将 Netdata Agent 添加到 Netdata Cloud

使用 docker exec 将已经在运行的节点添加到 Netdata

docker exec -it netdata netdata-claim.sh -token=TOKEN -rooms=ROOM1,ROOM2 -url=https://app.netdata.cloud

其中:

  • TOKEN 需要替换
  • ROOM1,ROOM2 替换成自己的

禁用本地面板

编辑 vi /etc/netdata/netdata.conf 配置文件:

找到 bind to 这样行,修改为:

[web]
    bind to = 127.0.0.1 ::1

然后重启 sudo systemctl restart netdata

对于 Docker 安装的,直接在配置中将 19999 的端口映射移除即可。


2021-06-09 netdata , monitor , netdata-cloud , linux , cpu , memory , bandwidth

Ubuntu 下启用 SFTP 服务

[[SFTP]] 全名为 SSH File Transfer Protocol,是一种通过 SSH(Secure Shell)协议进行文件传输的网路协议。它提供了一种安全的方式来传输文件,因为所有传输的数据都会被加密,这可以防止数据在传输过程中被拦截和阅读。 SFTP 不仅可以进行文件传输,还可以进行远程文件管理,例如创建和删除远程目录,移动和重命名远程文件等。这使得 SFTP 成为一种非常强大的工具,尤其是对于需要远程管理文件的开发者来说。

在互联网的早期人们都使用 FTP 来传输文件,FTP 是 File Transfer Protocol 文件传输协议的缩写,这是一个非常流行的文件传输协议。但因为 FTP 在传输过程中不是安全的,只能用于可信网络,所以就需要一个基于安全可靠连接的文件传输协议,这就是 SFTP(Secure File Transfer Protocol)。

SFTP 扩展了 SSH 协议,提供了安全的文件传输能力,提供了文件存取、传输和管理功能。

因为是基于 SSH 协议的,所以首先需要依赖 openssh-server

sudo apt install openssh-server
sudo apt install ssh
sudo vi /etc/ssh/sshd_config

文件末尾增加:

Match group sftp
ChrootDirectory /home
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
ForceCommand internal-sftp

然后重新加载配置:

sudo service ssh restart

创建 sftp 用户组:

sudo addgroup sftp

创建用户并将用户添加到用户组:

sudo useradd -m sftpuser -g sftp

修改密码:

sudo passwd sftpuser
sudo chmod 700 /home/sftpuser/

连接

sftp sftpuser@localhost

如果遇到错误

Connection closed

可以通过如下的方式查询错误日志

grep -i sftp /var/log/*

或者执行

sftp -vvv sftpuser@localhost

2021-06-07 sftp , ubuntu , linux , ssh , file , backup

macOS 上的超级强大的键盘自定义工具 Karabiner Elements

之前使用 macOS 外接键盘的时候因为想要实现和 Linux 一致的键位,所以接触到了 Karabiner Elements 这一款 macOS 上的键盘映射工具,但是了解之后发现,Karabiner 能做的事情不只有键盘按键的映射,设置可以组合按键,区别短按和长按,组合使用不同的按键,下面就简单的介绍一下过去几年里面我使用的 Karabiner Elements 特性。

Karabiner Elements 是 Mac 上一款强大的键盘自定义工具,几乎可以实现任何的键盘 remapping,并且也是开源的。如果说改键的话,只显示了 Karabiner 的很小一部分功能,Karabiner 还能实现组合按键触发一些功能,甚至可以针对不同的设备触发不同的功能。

官方网站: https://github.com/pqrs-org/Karabiner-Elements

功能特性

  • 键盘映射,将某一个功能按键映射到另一个功能键,比如把 Fn 映射成 Ctrl;或者调换按键的功能,比如当我连上我的外接键盘的时候就将左侧的 Option 键和 Cmd 键互换,这样按键的布局就和内置的键盘是一样的了。
  • 可以将一个按键映射成多个按键,比如把 caps lock 映射成 Ctrl+Shift+Option+Cmd,作为一个 hyper key,再使用该 hyper 就能实现一些快捷键,比如 hyper + hjkl 结合 Hammerspoon 来管理窗口
  • 将一个按键的短按(轻触),和长按区别开,比如 Caps Lock 按一下映射成 Esc,长按组合其他按键映射成同时按下 Ctrl+Shift+Option+Cmd,作为 hyper key。
  • 支持不同的 profile 配置,比如我就有两套 remapping, 分别对应内置的键盘和外接的键盘,利用自带的 cli 工具,再结合 Hammerspoon 就可以轻松的实现自动根据 WiFi 或者监听 USB 设备来切换 profile 配置;又或者可以配置在 Alfred 中配置快速切换 profile
  • 针对特定型号的键盘生效不同的配置
  • 支持虚拟键盘

使用 Karabiner 自带的 cli 切换 profile

Karabiner 自带一个叫做 karabiner_cli 的命令,使用该命令可以快速切换 profile。

这个命令默认没有加入到 PATH,需要完整的输入路径来执行:

/Library/Application\ Support/org.pqrs/Karabiner-Elements/bin/karabiner_cli --list-profile-names

查看其使用方式,可以清楚的看到可以用来快速的切换 profile 信息。

A command line utility of Karabiner-Elements.
Usage:
  karabiner_cli [OPTION...]

      --select-profile arg      Select a profile by name.
      --show-current-profile-name
                                Show current profile name
      --list-profile-names      Show all profile names
      --set-variables arg       Json string: {[key: string]: number}
      --copy-current-profile-to-system-default-profile
                                Copy the current profile to system default
                                profile.
      --remove-system-default-profile
                                Remove the system default profile.
      --lint-complex-modifications complex_modifications.json
                                Check complex_modifications.json
      --version                 Displays version.
      --version-number          Displays version_number.
      --help                    Print help.

Examples:
  karabiner_cli --select-profile 'Default profile'
  karabiner_cli --show-current-profile-name
  karabiner_cli --list-profile-names
  karabiner_cli --set-variables '{"cli_flag1":1, "cli_flag2":2}'

From: https://pqrs.org/osx/karabiner/document.html#command-line-interface

再结合 Hammerspoon 根据条件自动切换不同的 profile

wifiWatcher = nil
homeSSID = "EinVerne_5G"
lastSSID = hs.wifi.currentNetwork()

workSSID = "MIOffice-5G"

function ssidChangedCallback()
    newSSID = hs.wifi.currentNetwork()

    if newSSID == homeSSID and lastSSID ~= homeSSID then
        -- We just joined our home WiFi network
        hs.audiodevice.defaultOutputDevice():setVolume(25)
        hs.alert.show("Welcome home!")
        -- result = hs.network.configuration:setLocation("Home")
        -- hs.alert.show(result)
    elseif newSSID ~= homeSSID and lastSSID == homeSSID then
        -- We just departed our home WiFi network
        hs.audiodevice.defaultOutputDevice():setVolume(0)
        hs.alert.show("left home!")
        -- result = hs.network.configuration:setLocation("Automatic")
        -- hs.alert.show(result)
    end

    if newSSID == workSSID then
        hs.alert.show("work karabiner setup")
        hs.execute("'/Library/Application Support/org.pqrs/Karabiner-Elements/bin/karabiner_cli' --select-profile Work")
    else
        hs.alert.show("built-in karabiner setup")
        hs.execute("'/Library/Application Support/org.pqrs/Karabiner-Elements/bin/karabiner_cli' --select-profile Built-in")
    end

    lastSSID = newSSID
end

wifiWatcher = hs.wifi.watcher.new(ssidChangedCallback)
wifiWatcher:start()

设定 Caps Lock 作为 Hyper key

在菜单,Complex modifications 中可以开启 caps lock 作为 Cmd+Control+Option+Shift。

设定 Ctrl np 作为上下

从该 网站 导入 complex modifications,然后就可以找到 Emacs 的键位绑定,我开启了全局的 Ctrl+n/p 作为上下。

设定 Finder 重命名

默认情况下 Enter 是进入重命名,我对重命名的需求没有那么大,所以将 Linux 下的习惯 F2 映射成重命名。Enter 则是进入文件夹。

禁用 Cmd+h 最小化窗口

Cmd+h 在 macOS 上算是鸡肋,直接禁用。

reference


2021-05-29 mac-app , macos , mac-application , keyboard , karabiner , open-source

减小 git 仓库的大小

我一直使用 Git 仓库来管理我的 Markdown 笔记,但是因为定时提交,没多久就产生了非常多的提交历史,并且因为频繁的提交和导入了一些比较大的 PDF 文件和图片文件,所以导致 .git 目录的体积已经超过了所有笔记的大小,笔记内容也就 300+M,但是整个仓库有近 1G 大小。

所以便想着能不能给 Git 仓库进行一下瘦身,最开始想要实现的方向是能不能压缩一下提交历史,然后把历史记录中的大文件剔除。所以查询方案的时候就先往这两个方向上靠。

git gc

最先想到的就是在仓库执行 git gc, (garbage collection),这条命令会对 Git 仓库中不需要的文件进行删除,然后将其他文件压缩:

git gc --aggressive

然后再执行:

git prune

git-prune 命令会删除在 object database 中不可达的 objects。不过通常在执行 git gc 的时候会自动调用该命令。

我的仓库中执行这两条命令后效果并不是很明显。

删除 Git 提交的大文件

可以使用 git count-objects -v 来查看 git 仓库占用的空间大小。

count: 391
size: 13968
in-pack: 41519
packs: 2
size-pack: 493311
prune-packable: 0
garbage: 10
size-garbage: 0

在输出的结果中 size-pack 就是包文件的大小,单位是 KB,可以看到我本地的包文件在 493MB 左右。

使用如下的命令查看仓库中的大文件:

git rev-list --objects --all | grep -E `git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -10 | awk '{print$1}' | sed ':a;N;$!ba;s/\n/|/g'`

或者:

git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -15 | awk '{print$1}')"

解释:在 git gc 命令执行之后,所有的对象会被放到一个打包的文件中,存放在 .git/objects/pack/*.idx,可以通过 git verify-pack 命令,对输出的第三列(文件大小)进行排序,从而找出这个大文件。

然后使用 git rev-list 命令使用 --objects 参数来找出相关的 SHA-1,对象的 SHA-1 和相关联的文件路径。

然后可以使用 git filter-branch 来改写历史,移除大文件

# 下面的命令请谨慎执行,在多人合作的仓库中小心执行
git filter-branch --tree-filter 'rm -f path/to/large/files' --tag-name-filter cat -- --all
git push origin --tags --force
git push origin --all --force

说明:--tree-filter

这里的路径别搞错。

bfg

在搜寻的过程中发现了 git filter-branch 的代替工具 bfg 可以比 filter-branch 命令更快。

reference

In my case, I pushed several big (> 100Mb) files and then proceeded to remove them. But they were still in the history of my repo, so I had to remove them from it as well.

What did the trick was:

bfg -b 100M  # To remove all blobs from history, whose size is superior to 100Mb
git reflog expire --expire=now --all
git gc --prune=now --aggressive

Then, you need to push force on your branch:

git push origin <your_branch_name> --force

bfg

bfg 可以使用 brew 来安装:

brew install bfg
# 清除垃圾文件(大量无用的mp3文件)
git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch *.mp3' --prune-empty --tag-name-filter cat -- --all
# 提交到远程仓库(如GitHub, 我再次从git clone GitHub代码库会变小为1.3M)
git push origin --force --all
# 必须回收垃圾,本地仓库才变小
git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin
git reflog expire --expire=now --all
git gc --prune=now

rm -rf .git/refs/original
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

reference


2021-05-02 git , linux

读书是否是唯一重要的事?

不久之前和朋友约去了趟植物园,聊起读书是否是第一要务的时候产生了一些分歧,关于是否要去学习如何学习这一件事情产生了一些分歧。我站在的立场是读书是必须的,而我朋友则认为在有限的时间里面实践要优先于读书。而关于要不要学习如何学习这一件事情,他仍然坚持自己的实践而非去了解如何学习。

在我仔细思考,以及阅读了相关的文章之后慢慢地对我朋友的一些想法产生了一些理解。但在一些观点上我依然坚持我自己的看法。在我看来实践固然重要,但读书在我看来一样的重要,在我过去年的学习生活中,我曾经尝试过这种各样的媒介,但是没有一个能够提供图书所带来的成体系的知识。

两种读书的方式

首先我要来阐述一下,我自身的两种读书方式:

  • 为了快速获取[[知识]]而读书,比如学习一个概念或一门技术
  • 为了认识这个世界而读书,比如去理解什么是[[政治]],亦或是为了理解资本主义的运行方式而去看[[202009121000-牛津通识读本-资本主义]]。

恰好不久前看到 [[罗翔]] 老师的一段话,让我深有感触:

如果读书的目的是为了成功,其实很多书是没有必要读的,学习法律就天天读法律条文就可以了?还需要读莎士比亚吗?还需要读[[201910271449-论法的精神]]吗?没有必要啊,你的目的成功了,多读读成功学不就得了吗? 我们读书一定有两种目的,不否认有功利性的目的,比如要准备高考,要准备其他考试,但是更重要的一种读书是非功利性的读书,因为只有这种非功利性的读书,你才能够抵制成功主义的读书,你才能够坦然的接受自己的命运,坦然的接受自己的失败,一个人真正的成功不是在他辉煌的时候有多么的风光,但关键在于在挫折的时候,在低迷的时候,你是不是依然有勇气继续前行,这就是为什么我们依然要进行非功利性的阅读。因为只有非功利性的阅读,才能够给我们提供一种真正的人生勇气,去面对人生的大风大浪,每个人这一生一定会遭遇苦难,那在苦难的时候,你是否依然有继续活下去的勇气。

在我看来非功利性地读书才是真正让我可以静下来理解这个世界的方式,让我的心智可以更加接近真实的世界。

读书不是获取知识的唯一来源

虽然我认为读书是人进步非常重要的一环,但是我也知道读书不是获取[[知识]]唯一的来源。

既然提到了人类知识的来源,就要仔细的推敲人类的知识是从何而来的,[[洛克]]在[[人类理解论]]中对人类思维的方式做了概括性的描述:

  1. Combining several simple ideas into one compound one, and thus all complex ideas are made.
  2. The second is bringing two ideas, whether simple or complex, together, and setting them by one another so as to take a view of them at once, without uniting them into one, by which it gets all its ideas of relations.
  3. The third is separating them from all other ideas that accompany them in their real existence: this is called abstraction, and thus all its general ideas are made.

这一段话也被 Obsidian 描述在其帮助文档中,这一段话让我受益匪浅。那我继而追问,我们的 idea 从何而来,可以想到的便是,第一产生自我们自己(不管是根据自己的经验得来,还是观察这个社会总结而来),第二从别人那边吸收而来。

那么对应到我们的现实世界,我便认为从我们自身经验产生的 idea 可以以实践的方式获得,而从别人那边获取的 idea,以我自身的经验,图书便是非常重要的来源,当然如今这个信息化的时代还有许多其他的途径。任何产生新的 idea 的内容都是值得阅读的,对于走在一线的研究人员,期刊、文献可能是最重要的知识来源,对于在一线的开发人员,技术的实现原理 GitHub 上的代码可能是重要的知识来源。当认识到这一点之后便不会再局限与图书,读书固然是知识来源重要的一个来源,但是只要是能够产生新的 idea,并且可以和自己现有的 idea 产生新的碰撞的信息来源都是值得我们去阅读,了解的。

和我朋友所站的立场一样,我也认同实践的重要性,但是我认为光有实践是不行的。要从实践中获取经验,然后对经验反思总结和提炼,做到洛克在第三点中所说的那样,抽象化,形成新的思维模型和方法论。而这个「方法论」恰恰就是那些经典的图书所能带给我们的。

然而一旦通过图书获得了对某个观念,某个概念的基础认识之后,就应该通过理论去指导实践。读书无法代替实践,但是通过图书形成的理论可以让我走得更加自信。

读书的三个阶段

在[[202008161501-如何阅读一本书笔记]] 中作者 [[Mortimer J. Adler]] 提出了阅读的四个层次

  • [[基础阅读]]
  • [[检视阅读]]
  • [[分析阅读]]
  • [[主题阅读]]

在结合自身的经验,我发现我经过这样三个阶段:

  • 在最开始的时候不知道看什么,漫无目的的阅读,没有作笔记,也没有写评价,最后什么没有收获
  • 然后开始有目的的阅读,比如喜欢看某一位作者的书,比如 [[密尔]],或者是为了了解一个主题去阅读相关的书籍,比如去看 [[哲学]],又或者只是为了学习一门技术比如去读 [[Spring 知识点]],这个阶段会围绕同一个主题进行大量的阅读,通过大量的阅读积累对其的认识,并在自己心里产生基础的概念
  • 在有了一定的基本知识体系之后再去阅读是为了接受新的、不同的思想,并将其拆解开放到自己的知识体系中。如果以哲学家为例,在了解了其基本观念之后,逐渐添加我是否支持其观点,有没有反驳他的观点,我能不能找到佐证。又如果是一门技术,新产生的技术解决了之前的什么问题,又会产生什么新的问题,这个技术细节为什么要这样实现,有没有更好的实现方式。

但这三个阶段都有一点要非常注意的事情就是,当我们去阅读的过程中,不仅要去了解作者的观点,也要将作者的观点溶化形成我们自己的观点,[[密尔]]、[[洛克]] 所生活的时代早已经过去,我们如今要思考的是,这些先哲们的思考对当今的生活有什么启示。

图书存在的问题

在说了这么多读书的必要性之后,还有必要在谈一谈图书存在的问题,我并不认为每一本都值得去阅读,在上面的提及的 [[检视阅读]]中作者就说,在阅读时,你可以快速的先了解一下书的目录,在特定的时间中快速完成阅读,了解这是什么类型的图书,这本书在讨论什么内容。在有了这个基础之后再决定哪一部分内容需要仔细阅读,又或者可以直接不读,或者放着以后再读。

当以这样的方式进行读书的时候,会发现图书这样一些问题:

  • 有一些书因为出版时间较久,很多的内容已经过时,在技术领域尤其如此,往往几年时间技术就已经更新换代,之前书中的知识就便的没有作用,但是这个时候更要提醒自己,因为被代替便意味着有更好的方案出现,在哲学的领域也是一样,虽然观念在不断的变化,也也会从一个观念变化都另一个观念,这个观念的转变自身便是值得去了解的
  • 警惕作者大而全的理论框架,往往有一些作者想使用自己的理论框架去解释一切,这样的图书要不就是伟大的著作,要不就会在细节部分不断的被别人攻破,理性地看待这样的著作。

从被动接受信息变成主动获取信息

在经过了上面的三个阶段之后,会发现自身不自觉的从一种被动的接受信息的状态,变成了主动探索信息的状态。我很早以前就使用 InoReader 来作为每天的新闻信息来源,但是我发现这样的方式仅仅停留在了第一个阶段,看过即忘,没有形成自己的体系,所以渐渐的就不再去追寻最新的新闻,慢慢的从这个嘈杂的新消息中脱离出来了。

那这个时候产生的问题便是如何以最快的速度获取信息,我的方法便是「搜索」,我发现我关心的事情也不会那么多,有什么我不清楚或我想要了解的主题时,我便使用搜索的方式,先确定我大致可以参考的书目,比如最近我想要了解[[期权]],那么我先看看 Kindle Unlimited 中包含这个词的书有哪些,再 Google 搜索一下推荐书目,看过两三个页面之后大概就会有一个基础印象,那么接下来就是完成这些图书的阅读,并且总结出自己的知识体系,然后通过这个知识体系来指导我的实践。然后在真实的操作之中总结出自己的理论,再放入到自己的知识库中(对我而言就是我的Zettelkasten笔记库,和博客)。

[[主动学习]],有针对性的获取信息,不仅可以避免自己陷入信息的汪洋,也可以扩展自己的认知边界,将新的、不同的理论纳入到自己的知识体系,不断地完善自己零散的思维模式。最关键的是,当不断阅读自己的感兴趣的内容时,会自发的产生兴奋的状态,并且可以不断往外延伸出不同的主题,当我去了解洛克的时候,我又发现了[[霍布斯]],当再去了解[[西方哲学史]]的时候,又追溯到[[柏拉图]],[[亚里士多德]]等等,而当我对[[期权]]感兴趣的时候,又会好奇郁金香球茎泡沫事件的发生,继而不断地发现新的好奇的事物。


2021-05-01 study , learning , thinking , discuss , reading , book

Git 使用技巧:创建不带历史的分支

有些时候想要创建一个不带历史记录的 git 分支,比如要从原来在本地开发的项目中,将代码 push 到 GitHub 开源,不想分享糟糕的历史提交记录,那就可以创建一个不带历史记录的分支。

查看 git checkout --help 的帮助说明, 可以看到其中有一个选项是 --orphan ,就是创建一个孤立的分支,这个分支上的第一个提交不回有任何的 parents 节点。

       --orphan <new-branch>
           Create a new orphan branch, named <new-branch>, started from <start-point> and switch to it. The first commit made on this new branch will have no parents and it will be the
           root of a new history totally disconnected from all the other branches and commits.

所以我们可以做如下的操作:

git checkout --orphan new_branch
git add .
git commit -m "Init commit"
git push -u origin new_branch

2021-04-19 git , git-history , git-branch , git-checkout , git-tips

电子书

本站提供服务

最近文章

  • Dinox 又一款 AI 语音实时转录工具 前两天介绍过 [[Voicenotes]],也是一款 AI 转录文字的笔记软件,之前在调查 Voicenotes 的时候就留意到了 Dinox,因为是在小红书留意到的,所以猜测应该是国内的某位独立开发者的作品,整个应用使用起来也比较舒服,但相较于 Voicenotes,Dinox 更偏向于一个手机端的笔记软件,因为他整体的设计中没有将语音作为首选,用户也可以添加文字的笔记,反而在 Voicenotes 中,语音作为了所有笔记的首选,当然 Voicenotes 也可以自己编辑笔记,但是语音是它的核心。
  • 音流:一款支持 Navidrom 兼容 Subsonic 的跨平台音乐播放器 之前一篇文章介绍了Navidrome,搭建了一个自己在线音乐流媒体库,把我本地通过 [[Syncthing]] 同步的 80 G 音乐导入了。自己也尝试了 Navidrome 官网列出的 Subsonic 兼容客户端 [[substreamer]],以及 macOS 上面的 [[Sonixd]],体验都还不错。但是在了解的过程中又发现了一款中文名叫做「音流」(英文 Stream Music)的应用,初步体验了一下感觉还不错,所以分享出来。
  • 泰国 DTV 数字游民签证 泰国一直是 [[Digital Nomad]] 数字游民青睐的选择地,尤其是清迈以其优美的自然环境、低廉的生活成本和友好的社区氛围而闻名。许多数字游民选择在泰国清迈定居,可以在清迈租用廉价的公寓或民宿,享受美食和文化,并与其他数字游民分享经验和资源。
  • VoceChat 一款可以自托管的在线聊天室 VoceChat 是一款使用 Rust(后端),React(前端),Flutter(移动端)开发的,开源,支持独立部署的在线聊天服务。VoceChat 非常轻量,后端服务只有 15MB 的大小,打包的 Docker 镜像文件也只有 61 MB,VoceChat 可部署在任何的服务器上。
  • 结合了 Google 和 AI 的对话搜索引擎:Perplexity AI 在日本,因为 SoftBank 和 Perplexity AI 开展了合作 ,所以最近大量的使用 Perplexity ,这一篇文章就总结一下 Perplexity 的优势和使用技巧。