pyftpdlibとDjangoを使ったFTPサーバ

自宅サーバ用でProFTPDを使っていたのだが、ユーザー管理が面倒なのでFTPサーバを自作してみた。

Djangoのユーザー管理機能に結びつけたFtpUserモデルを作成し、pyftpdlibのauthorizerを書いただけのシンプルなもの。
settings.py

# Django settings for unboxftpd project.
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

DEBUG = True
TEMPLATE_DEBUG = DEBUG

ADMINS = (
    ('admin', 'admin@hoge.hoge')
)

MANAGERS = ADMINS

DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = os.path.join(BASE_DIR, "unboxftpd.db")
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.

TIME_ZONE = 'Asia/Tokyo'

LANGUAGE_CODE = 'ja'

SITE_ID = 1

USE_I18N = True

MEDIA_ROOT = ''

MEDIA_URL = ''

ADMIN_MEDIA_PREFIX = '/media/'

SECRET_KEY = 'secret_key..'

TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.load_template_source',
    'django.template.loaders.app_directories.load_template_source',
)

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.middleware.doc.XViewMiddleware',
)

ROOT_URLCONF = 'unboxftpd.urls'

TEMPLATE_DIRS = (
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
    'unboxftpd.ftpd',
)

urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'', include('django.contrib.admin.urls')),
)

ftpd/models.py

# -*- coding: utf8 -*-
from django.db import models
from django.contrib.auth.models import User
import datetime

class FtpUser(models.Model):
    """
    unboxftp users model
    """
    user = models.ForeignKey(User)
    last_login = models.DateTimeField(default=datetime.datetime.now(), editable=False)
    root_dir = models.CharField(null=False, maxlength=250)
    readable = models.BooleanField(default=True)
    writable = models.BooleanField(default=False)

    class Admin:
        list_display = ('user', 'root_dir', 'last_login', 'readable', 'writable',)
        search_fields = ('user',)
        fields = (
            (None, {'fields': ('user', 'root_dir',)}),
            ('Permission', {'fields': ('readable', 'writable',)}),
        )

    def __str__(self):
        return self.user.username

    def login_update(self):
        self.last_login = datetime.datetime.now()

ftpd/ftpdconf.py

# -*- coding: utf8 -*-
ADDRESS = '127.0.0.1'
PORT = 21

ftpd/ftpd.py

# -*- coding: utf8 -*-
import os
import sys

BASE_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../'))
if not BASE_DIR in sys.path:
    sys.path.append(BASE_DIR)
os.environ['DJANGO_SETTINGS_MODULE'] = 'unboxftpd.settings'
from django.contrib.auth.models import User

from pyftpdlib import ftpserver
import ftpdconf

class UnboxFtpdAuthorizer:

    def __init__(self):
        pass

    def validate_authentication(self, username, password):
        try:
            u = self.get_user(username=username)
            return u.check_password(password)
        except:
            return False

    def has_user(self, username):
        try:
            u = self.get_user(username=username)
            return True
        except:
            return False

    def get_home_dir(self, username):
        try:
            u = self.get_ftpuser(username=username)
            return u.root_dir
        except:
            return None

    def get_msg_login(self, username):
        u = self.get_ftpuser(username=username)
        u.login_update()
        u.save()
        return 'welcome to unboxftp server'

    def get_msg_quit(self, username):
        return 'see you next time'

    def r_perm(self, username, obj=None):
        try:
            u = self.get_ftpuser(username=username)
            return u.readable
        except:
            return False

    def w_perm(self, username, obj=None):
        try:
            u = self.get_ftpuser(username=username)
            return u.writable
        except:
            return False

    def get_user(self, username):
        try:
            return User.objects.get(username=username)
        except:
            raise

    def get_ftpuser(self, username):
        try:
            return User.objects.get(username=username).ftpuser_set.all()[0]
        except:
            raise

def runftpd():
    authorizer = UnboxFtpdAuthorizer()
    ftp_handler = ftpserver.FTPHandler
    ftp_handler.authorizer = authorizer
    address=(ftpdconf.ADDRESS,ftpdconf.PORT)
    ftpd=ftpserver.FTPServer(address,ftp_handler)
    ftpd.serve_forever()

if __name__ == '__main__':
    runftpd()

http://localhost:8000/ にアクセスするとおなじみのadmin画面。ftpdは、クライアントがログインするときに毎回DBを参照しに行くので、ユーザーの追加・削除を行っても再起動の必要はない。
ソースのアーカイブは以下に。
unboxftpd20071101.zip

2008/1/23追記

この記事はpyftpdlib 0.2.0の時点で書いたものです。
pyftpdlib 0.3.0の場合はパーミッションの実装に変更があるので注意してください。

2016/4/13追記

もろもろ整理してライブラリ化したものをdjango-ftpserverとして公開しています。
GitHub - tokibito/django-ftpserver: FTP server application that used user authentication of Django.