UbuntuでTaildropによるファイル送信を手軽に

Tailscaleでネットワークを構築していると多数の便利な機能が利用出来ます。Tailscaleネット参加デバイス同士で手軽にファイル送信出来るTaildropもその一つです。
なお、WindowsやMacからならGUIで手軽に送信出来ますが、UbuntuでTaildropを使ってファイルを送るには、主にターミナル(コマンド)を使用します。

Taildropによるファイル送信するコマンド

ターミナルを開き、以下のコマンド形式で実行します:

tailscale file cp <送信したいファイル名> <送信先のデバイス名>:
  • デバイス名: tailscale status コマンドで確認できる、宛先デバイスの名前(例:my-iphonewindows-laptop)です。
  • 注意: デバイス名の最後には必ず :(コロン) を付けてください。

ちなみに、Tailscale公式が提供している軽量なトレイアイコンアプリはありますが、出来る事は接続状態の確認とExit Nodeの切り替え程度です。

tailscale systray

UbuntuでTailscaleをGUIで使う方法

Ubuntuには、公式のフルGUIクライアントが存在しないため、「右クリックで送信」などの標準的なGUI機能はありません。しかし、非公式GUIツール「Trayscale」を使えば、GUIで送信出来るようになるようです。

  • Tailscaleの接続状態の確認
  • Exit Nodeの切り替え
  • Taildropによるファイルの送受信管理
  • その他、さまざまな操作をGUIで実行可能

Ubuntuデスクトップなどを使うなら便利そうえすね。また、KDEなどでは、スクリプトを導入することで右クリックメニューに「Taildropで送信」を追加できるようです。

スクリプトを使用して簡単送信

ただ、送信するだけなら、スクリプトを作って保存しておくだけでも十分でしょう。Ubuntu Serverなど、GUIが無い環境でも手軽です。
ここで紹介するスクリプトを実施すると、次のような手順で送信出来ます。

  1. 起動すると現在のフォルダのファイル一覧(ファイルサイズ付き)を表示
  2. 番号で送信ファイルを選択
  3. Tailnetに接続中のデバイス一覧(IPアドレス・オンライン状態付き)を表示
  4. 番号で送信先を選択
  5. 内容確認後、y で送信実行
# 現在のユーザーが管理者権限なしでTailscaleを操作できるように
sudo tailscale set --operator=$USER
# ディレクトリを作成してスクリプトを保存して実行
sudo mkdir -p /opt/script
cd /opt/script
sudo nano taildrop-send.sh
sudo chmod +x taildrop-send.sh
#!/usr/bin/env bash
# taildrop-send.sh — Taildropでファイルを送信するスクリプト

set -euo pipefail

BOLD='\033[1m'
CYAN='\033[1;36m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
RED='\033[1;31m'
DIM='\033[2m'
RESET='\033[0m'

info()    { echo -e "${CYAN}${BOLD}→${RESET} $*"; }
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
warn()    { echo -e "${YELLOW}${BOLD}!${RESET} $*"; }
error()   { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
sep()     { echo -e "${DIM}$(printf '─%.0s' {1..52})${RESET}"; }

# ── 依存チェック ────────────────────────────────────────
check_dependencies() {
    if ! command -v tailscale &>/dev/null; then
        error "tailscale コマンドが見つかりません。"
        echo "  インストール: https://tailscale.com/download/linux"
        exit 1
    fi
    if ! tailscale status &>/dev/null; then
        error "Tailscale が起動していません。"
        echo "  起動: sudo systemctl start tailscaled && sudo tailscale up"
        exit 1
    fi
}

# ── ファイル一覧を表示・選択 ────────────────────────────
select_file() {
    echo ""
    echo -e "${BOLD}📁 現在のフォルダのファイル一覧${RESET}  ${DIM}$(pwd)${RESET}"
    sep

    mapfile -t FILES < <(find . -maxdepth 1 -type f ! -name '.*' | sed 's|^\./||' | sort)

    if [[ ${#FILES[@]} -eq 0 ]]; then
        error "ファイルが見つかりません: $(pwd)"
        exit 1
    fi

    local i=1
    for f in "${FILES[@]}"; do
        local size
        size=$(du -sh "$f" 2>/dev/null | cut -f1)
        printf "  ${CYAN}%3d${RESET}  %-42s ${DIM}%s${RESET}\n" "$i" "$f" "$size"
        ((i++))
    done
    sep
    echo ""

    while true; do
        read -rp "$(echo -e "送信するファイルの番号を入力 ${DIM}[1-${#FILES[@]}]${RESET}: ")" file_choice
        if [[ "$file_choice" =~ ^[0-9]+$ ]] && \
           (( file_choice >= 1 && file_choice <= ${#FILES[@]} )); then
            SELECTED_FILE="${FILES[$((file_choice - 1))]}"
            success "選択したファイル: ${BOLD}${SELECTED_FILE}${RESET}"
            break
        else
            warn "無効な番号です。1〜${#FILES[@]} の範囲で入力してください。"
        fi
    done
}

# ── 送信先一覧を表示・選択 ──────────────────────────────
select_target() {
    echo ""
    echo -e "${BOLD}💻 Tailnetのデバイス一覧${RESET}"
    sep

    # tailscale status の出力形式:
    #   <IP>  <hostname>  <user>  <os>  <status...>
    # 自分自身のIPを取得して除外する
    local self_ip
    self_ip=$(tailscale ip -4 2>/dev/null || echo "SELF")

    mapfile -t STATUS_LINES < <(
        tailscale status 2>/dev/null \
        | awk 'NF>=2 && $1!~/^#/ && !/^$/' \
        | grep -v "^${self_ip}" || true
    )

    declare -a PEER_NAMES=()
    declare -a PEER_IPS=()
    declare -a PEER_STATUS=()

    for line in "${STATUS_LINES[@]}"; do
        local ip hostname rest
        ip=$(echo "$line" | awk '{print $1}')
        hostname=$(echo "$line" | awk '{print $2}')
        rest=$(echo "$line" | cut -d' ' -f3-)

        # "offline" という単語が含まれる → オフライン、それ以外 → オンライン
        local flag
        if echo "$rest" | grep -qwi 'offline'; then
            flag="オフライン"
        else
            flag="オンライン"
        fi

        PEER_NAMES+=("$hostname")
        PEER_IPS+=("$ip")
        PEER_STATUS+=("$flag")
    done

    if [[ ${#PEER_NAMES[@]} -eq 0 ]]; then
        error "接続可能なデバイスが見つかりません。"
        exit 1
    fi

    local i=1
    for idx in "${!PEER_NAMES[@]}"; do
        local color="${DIM}"
        [[ "${PEER_STATUS[$idx]}" == "オンライン" ]] && color="${GREEN}"
        printf "  ${CYAN}%3d${RESET}  %-30s ${DIM}%-20s${RESET} ${color}%s${RESET}\n" \
            "$i" "${PEER_NAMES[$idx]}" "${PEER_IPS[$idx]}" "${PEER_STATUS[$idx]}"
        ((i++))
    done
    sep
    echo ""

    while true; do
        read -rp "$(echo -e "送信先の番号を入力 ${DIM}[1-${#PEER_NAMES[@]}]${RESET}: ")" target_choice
        if [[ "$target_choice" =~ ^[0-9]+$ ]] && \
           (( target_choice >= 1 && target_choice <= ${#PEER_NAMES[@]} )); then
            SELECTED_TARGET="${PEER_NAMES[$((target_choice - 1))]}"
            SELECTED_IP="${PEER_IPS[$((target_choice - 1))]}"
            success "送信先: ${BOLD}${SELECTED_TARGET}${RESET} (${SELECTED_IP})"
            break
        else
            warn "無効な番号です。1〜${#PEER_NAMES[@]} の範囲で入力してください。"
        fi
    done
}

# ── 送信 ───────────────────────────────────────────────
send_file() {
    echo ""
    sep
    echo -e "  ${BOLD}送信内容の確認${RESET}"
    sep
    printf "  %-10s ${BOLD}%s${RESET}\n" "ファイル:" "${SELECTED_FILE}"
    printf "  %-10s ${BOLD}%s${RESET} ${DIM}(%s)${RESET}\n" "送信先:" "${SELECTED_TARGET}" "${SELECTED_IP}"
    sep
    echo ""

    read -rp "$(echo -e "送信しますか? ${DIM}[y/N]${RESET}: ")" confirm
    [[ "$confirm" =~ ^[yY] ]] || { echo ""; warn "キャンセルしました。"; exit 0; }

    echo ""
    info "送信中..."

    # まず sudo なしで試し、Access denied なら sudo で自動再試行
    if tailscale file cp "$SELECTED_FILE" "${SELECTED_TARGET}:" 2>/tmp/_taildrop_err; then
        echo ""
        success "送信完了: ${BOLD}${SELECTED_FILE}${RESET} → ${BOLD}${SELECTED_TARGET}${RESET}"
    else
        local errmsg
        errmsg=$(cat /tmp/_taildrop_err 2>/dev/null || echo "")
        if echo "$errmsg" | grep -qi "access denied\|permission denied"; then
            warn "権限エラー検出 → sudo で再試行します..."
            if sudo tailscale file cp "$SELECTED_FILE" "${SELECTED_TARGET}:"; then
                echo ""
                success "送信完了: ${BOLD}${SELECTED_FILE}${RESET} → ${BOLD}${SELECTED_TARGET}${RESET}"
                echo ""
                echo -e "  ${DIM}毎回 sudo を省略したい場合:${RESET}"
                echo -e "  ${CYAN}  sudo tailscale set --operator=\$USER${RESET}"
            else
                echo ""
                error "送信に失敗しました(sudo でも失敗)。"
                echo "$errmsg"
                exit 1
            fi
        else
            echo ""
            error "送信に失敗しました。"
            echo "$errmsg"
            exit 1
        fi
    fi
    echo ""
}

# ── メイン ──────────────────────────────────────────────
main() {
    clear
    echo ""
    echo -e "  ${BOLD}${CYAN}Taildrop ファイル送信ツール${RESET}"
    echo ""
    check_dependencies
    select_file
    select_target
    send_file
}

main "$@"

あとは、送信したいファイルがあるフォルダで下記を実施します。

bash /opt/script/taildrop-send.sh

受信側

受信側では、下記コマンドで受信したファイルを任意のフォルダに保存できます。

tailscale file get .
  • tailscale コマンドとTailscaleデーモンが起動している必要があります
  • tailscale file cpsudo 不要で使えますが、環境によっては権限エラーが出る場合があります。その際は sudo tailscale file cp ... に変更してください

受信側(CLI)を自動化したい場合。もし受信側がLinuxなどで、いちいち file get を打つのが面倒な場合は、以下のようなループスクリプト(ワンライナー)で「自動受け取り」状態にできます。

while true; do tailscale file get . && tailscale file status; sleep 2; done

受信側自動化

受信側でもっと便利なのは、あらかじめ下記を実行しておくことです。受け取ったデータを指定したディレクトに自動保存します(ここでは/opt/lxd-data/taildropに保存する設定にしています。このブログで扱うLXDコンテナの内容の場合は、どのコンテナからも参照出来て便利)。
下記スクリプトではサービス化しているので、再起動しても自動実行されます。

TARGET_DIR="/opt/lxd-data/taildrop"

# ディレクトリが存在しない場合は作成
sudo mkdir -p "$TARGET_DIR"

# サービスファイル作成(sudo tee を使う)
sudo tee /etc/systemd/system/taildrop.service > /dev/null <<EOF
[Unit]
Description=Tailscale File Receiver (Taildrop)
After=network.target tailscaled.service
Requires=tailscaled.service

[Service]
Type=simple
ExecStart=tailscale file get --loop --conflict=rename $TARGET_DIR
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# 有効化・起動
sudo systemctl daemon-reload
sudo systemctl enable taildrop
sudo systemctl start taildrop

# 確認(Ctrl+Cで終了)
sudo systemctl status taildrop
タイトルとURLをコピーしました