見出し画像

ファイルのアップロードから短縮URL取得まで一気に行うPythonスクリプトの作成

僕はクラウドストレージの同期ソフトを常駐させるのがとにかく嫌いなので、特にGoogle Driveはブラウザーで使っています。というのも、クラウドで触るのはデータ周りが多いのでローカルと同期する必要はほとんどなくて、たまにちょっとしたファイルをサクッと共有する必要があるぐらいなんですよね。するとファイルをクラウドストレージに(一方的に)アップロードしつつ短縮した共有リンクがすぐ返ってきたら便利だなーなんて思っていて、しかしなかなか一気通貫のコードを書く余裕がなかったんですよね。
ということで、今日はちょっと時間があるので勉強がてらやってみる。各種APIを使ってアプリ化するので、とりあえずプロジェクト名をpubdistとしておくこととする。

*短縮URLがそもそも結構害悪みたいなところもあるので(悪意のあるウェブサイトに飛ばされてもわからない)、正直それ自体あんま多用しない方がいいとは思いますけど、今回は自分の興味もあって作っています。

処理の流れ

いま僕が実装したいことの流れをざっとまとめるとこんな感じ。
1. 指定したファイルをクラウドストレージにアップロード
2. アップロードしたファイルの共有設定を変更しパブリックリンクを取得
3. 共有リンクの短縮URLを取得
4. GUIにする
最低限の機能としての1-3は非常に簡単に実装できそうな予感がある。むしろ何を使って実装するか迷うぐらい。
macOSでやるのでAlfredのショートカットにしてもいいけど、最終的には4にあるとおりファイルをドラッグ&ドロップで放り込んで処理してもらえる感じにしたい。

基本的に全部Pythonで実装する。早速やっていく。

1. クラウドストレージにファイルをアップロード

APIが使いやすそうなDropboxを使うことに。とりあえずDropbox Developersのアプリコンソールからアプリを作成してTokenを発行する。

アプリのパーミッションの設定も必要。今回は書き込みと共有設定がしたいので、
・files.content.read
・files.content.write
・sharing.write
にチェックを入れておく。


今回はトークンをmacOSの環境変数に突っ込んでおく。

export PUBDIST_DROPBOX_TOKEN="abc123"

こうすることによってPythonからos.getenv('PUBDIST_DROPBOX_TOKEN')でトークンを取得できるようになる。

Dropboxでファイルをいじるには、dropboxでトークンを使ってclientを作るところから始まる(詳しくは公式ドキュメントのSample appsのうちback-up-and-restore.pyがわかりやすい)。
ということでまずはclientの実装から。公式に則ってdbxという形でclientを作る。試しにユーザー情報を取得して表示してみる。(まずpip install dropboxでdropboxのPython SDKを入れておくこと)

import dropbox
DB_TOKEN = os.environ.get('PUBDIST_DROPBOX_TOKEN', '')

with dropbox.Dropbox(DB_TOKEN) as dbx:
    user = dbx.users_get_current_account()
    print(user)

これで'FullAccount(account_id='dbid:AABBCC~~~)'と返ってくればOK。

次にファイルをアップロードする関数を書いてみる。トークンが通ればdbx.files_uploadで簡単にファイルのアップロードができる。とりあえずサンプルファイルをmacOSのホームに置いておくけど、ここには書きたくないので環境変数からosで引っ張ってくる(os.environ['HOME'])。

import os, sys

local_dir = os.environ['HOME']+'/'
filename  = 'sample.png'
dest = os.path.join('target_directory', filename)

def upload():
    with open(local_dir+filename, 'rb') as f:
        try:
            meta = dbx.files_upload(f.read(), dest, mode=dropbox.files.WriteMode('overwrite'))
            newfile = meta.path_lower
            print(newfile)
            return newfile
        except dropbox.exceptions.ApiError as err:
            if (err.error.is_path() and
                    err.error.get_path().reason.is_insufficient_space()):
                sys.exit("ERROR: Insufficient space.")
            elif err.user_message_text:
                print(err.user_message_text)
            else:
                print(err)

with dropbox.Dropbox(DB_TOKEN) as dbx:
    user = dbx.users_get_current_account()
    newfile = upload()

dbx.files_upload関数はアップロードが完了するとメタデータを返してくれる。そこにファイル名や絶対パスなどが全部入っているため使いやすい。
今取得したメタデータからもう一回ファイル情報を取得できるかチェックしてみる。

def check_metadata(file_path):
    try:
        metadata = dbx.files_get_metadata(file_path)
        print(metadata)
        return True
    except dropbox.exceptions.ApiError as err:
        print(err)

with dropbox.Dropbox(DB_TOKEN) as dbx:
    user = dbx.users_get_current_account()
    newfile = upload()
    print('Uploaded file:', newfile)
    check_metadata(newfile)

こんな感じでアップロードしたファイルの情報が取得できたので、最後にパブリックリンクを取得する。これも同じくdbx内のsharing_create_shared_link_with_settings関数で簡単に取得できる。

def get_sharable_link(file_path):
    try:
        pub_link = dbx.sharing_create_shared_link_with_settings(newfile)
        print(pub_link)
        return pub_link.url
    except dropbox.exceptions.ApiError as err:
        print(err)

with dropbox.Dropbox(DB_TOKEN) as dbx:
    user = dbx.users_get_current_account()
    newfile = upload()
    print('Uploaded file:', newfile)
    check_metadata(newfile)
    public_url = get_sharable_link(newfile)
    print(public_url)

試しにプライベートブラウズでpublic_urlを叩けばちゃんと表示されることがわかるはず。

2. Bitlyで短縮URLを取得

次に、この長ったらしいURLを短縮する。これは短縮URLサービスのbit.lyで簡単にできる。Bitly DeveloperからAccess Tokenを生成する(https://app.bitly.com/settings/api/)。

取得したトークンは先ほどと同様に環境変数に突っ込んでおく。

export PUBDIST_BITLY_TOKEN="abc123"

あとは先ほど取得したpublic_urlをlog_urlに指定したJSONと一緒にPOSTするだけで、短縮URLが返ってくる。今回はBitly API (4.0.0)を使い、ヘッダーも作ってrequestsでPOSTする。将来的に希望するURLで短縮リンクを取得できるようdesired_urlという引数を作るだけ作っておく(ただ、Documentを眺める限りは、ある希望URLが既に存在しているかどうかを調べるAPIがない気がする。先に存在を調べて、ないことがわかってから希望URL付きでPOSTしたい)。

import requests
BL_TOKEN = os.environ.get('PUBDIST_BITLY_TOKEN', '')
POST_URL = 'https://api-ssl.bitly.com/v4/shorten'

def shorten_url(BL_TOKEN, public_url, desired_url=None):
    headers = {"Authorization": f"Bearer {BL_TOKEN}", "Content-Type": "application/json"}
    body    = {"domain": "bit.ly", "long_url": public_url}
    res = requests.post(POST_URL, headers=headers, json=body).json()
    return res["link"]

ちなみに"domain"の値はBitlyが提供している他の短縮ドメインにすることも可能。個人的にどうでもいいので割愛。

3. ファイルパスを引数に取る形に書き換える

ちょっと回りくどいけど、今回はデバッグ用に引数がない場合にはデフォルトでDesktopに置かれた画像がアップロードされるようにしておく。引数があったら実際にファイルが存在している場合にのみアップロード元のパスを上書きする。

local_dir = os.environ['HOME']
filename  = 'sample.png'
if len(sys.argv) > 1:
    file_path = sys.argv[1]
    if os.path.isfile(file_path):
        local_dir, filename = os.path.split(file_path)
local_abs = os.path.join(local_dir, filename)

4. ここまでの作業をまとめる。

ここまでに書いた内容をまとめて示すと(個別のコードをちょっとずつ直したのでところどころ違うかもしれない。一番下に貼ってあるけどGitHub上の最新版を見てもらったほうがいい)、

import os, sys, requests, dropbox
from random import random

local_dir = os.environ['HOME']
filename  = 'sample.png'
if len(sys.argv) > 1:
    file_path = sys.argv[1]
    if os.path.isfile(file_path):
        local_dir, filename = os.path.split(file_path)
local_abs = os.path.join(local_dir, filename)

dest = '/'+str(int(random()*1000000000))+'/'+filename
DB_TOKEN = os.environ.get('PUBDIST_DROPBOX_TOKEN', '')
BL_TOKEN = os.environ.get('PUBDIST_BITLY_TOKEN', '')
POST_URL = 'https://api-ssl.bitly.com/v4/shorten'

def upload():
    with open(local_abs, 'rb') as f:
        try:
            meta = dbx.files_upload(f.read(), dest, mode=dropbox.files.WriteMode('overwrite'))
            #newfile = '/Apps/public_uploader'+dest+filename
            newfile = meta.path_lower
            print(newfile)
            return newfile
        except dropbox.exceptions.ApiError as err:
            if (err.error.is_path() and
                    err.error.get_path().reason.is_insufficient_space()):
                sys.exit("ERROR: Insufficient space.")
            elif err.user_message_text:
                print(err.user_message_text)
                sys.exit()
            else:
                print(err)
                sys.exit()

def check_metadata(file_path):
    try:
        metadata = dbx.files_get_metadata(file_path)
        return True
    except dropbox.exceptions.ApiError as err:
        print(err)

def get_sharable_link(file_path):
    try:
        pub_link = dbx.sharing_create_shared_link_with_settings(newfile)
        print(pub_link.url)
        return pub_link.url
    except dropbox.exceptions.ApiError as err:
        print(err)

def shorten_url(BL_TOKEN, public_url, desired_url=None):
    headers = {"Authorization": f"Bearer {BL_TOKEN}", "Content-Type": "application/json"}
    body    = {"domain": "bit.ly", "long_url": public_url}
    res = requests.post(POST_URL, headers=headers, json=body).json()
    return res["link"]

if __name__ == '__main__':
    if len(DB_TOKEN)==0:
        raise ValueError('Dropbox token is not set')
    with dropbox.Dropbox(DB_TOKEN) as dbx:
        user = dbx.users_get_current_account()
        #print(user)
        newfile = upload()
        #print('Uploaded file:', newfile)
        check_metadata(newfile)
        public_url = get_sharable_link(newfile)

    if len(BL_TOKEN)==0:
        raise ValueError('bitly Token is not set.')
    else:
        short_link = shorten_url(BL_TOKEN=BL_TOKEN, public_url=public_url)
        print(short_link)

特に外に出してまずいものは残してないはず。
んでこれを実行してあげると、

まあ別に隠す必要もないんだけど。

こんな感じでdropbox上のパブリックURLと短縮後のURLが表示される。
*最終的なshort_link以外のprintは邪魔なので全部消してます。
*anacondaやらGitHubやらで環境がぐちゃぐちゃなのがバレますね。

あとやるべきこと

とりあえず今後やるべきことを列挙しておく。

  • GUIにして、ファイルをドラッグするだけで短縮URLがクリップボードに突っ込まれるようにする。(macOSのApp Cleanerみたいに、できればアイコンにドラッグするだけで動作する感じにしたいけど、とりあえずmacOSのショートカットに追加するだけでも結構便利な気がする。それこそドラッグ&ドロップでファイルも指定できるし)

  • リンク作成から30分(あるいは1時間とか指定した時間)の時間経過でdropbox上の共有が止まるようにする。ローカルで30分間sleepでセッション維持させるのはあまりにも非効率なので(かといってAWS Lambdaとか使うほどのことでもない)、たぶんIFTTTとかこねくり回して実装する。

GitHubにリポジトリ作っておきました

これといって大したことでもないのだけれど、適当に公開しておくことにします。


5. とりあえずPyQt5でGUIも作る

とりあえずPyQt5で適当に形だけ作っておく。機能は何もない。

import sys
from PyQt5 import QtCore as qtc
from PyQt5 import QtWidgets as qtw
import sip

class MainWindow(qtw.QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.setGeometry(300, 50, 400, 350)
        self.setWindowTitle('Instant Share')

        base_x = 50
        box_width  = 300
        box_height = 200

        self.title = qtw.QLabel('Instant Sharer', self)
        self.title.setAlignment(qtc.Qt.AlignCenter)
        self.title.resize(box_width, 20)
        self.title.move(base_x, 10)

        drag_y = 40
        self.label = qtw.QLabel('Drag & Drop Here', self)
        self.label.setAlignment(qtc.Qt.AlignCenter)
        self.label.setStyleSheet("color: black; background-color: lightgrey;")
        self.label.resize(box_width, box_height)
        self.label.move(base_x, drag_y)

        filepath_x = base_x
        filepath_y = drag_y + box_height + 30
        self.filepath_text = qtw.QTextEdit('File Path:',self)
        self.filepath_text.setStyleSheet("background-color: transparent;")
        self.filepath_text.move(filepath_x,filepath_y-20)

        self.filepath = qtw.QLineEdit(self)
        self.filepath.move(filepath_x+4,filepath_y)
        self.filepath.resize(box_width,20)

        button_x = base_x
        button_y = filepath_y+30
        button_width = 160
        self.button = qtw.QPushButton('Upload', self)
        self.button.resize(button_width,40)
        self.button.move(button_x, button_y)

        checkbox_x = button_x + button_width + 5
        checkbox_y = button_y
        self.needShorten = qtw.QCheckBox('Shorten the URL', self)
        self.needShorten.move(checkbox_x, checkbox_y)

        self.needPublic = qtw.QCheckBox('Make it Full Public', self)
        self.needPublic.move(checkbox_x, checkbox_y+20)

        #self.button.clicked.connect(self.output)
if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

いいなと思ったら応援しよう!