Skip to main content
 首页 » 编程设计

python之使用 FTP 后端的 Django 文件上传

2024年10月24日35fff_TT

我想根据示例上传我的文件 Need a minimal Django file upload example ,但是我不想将文件存储在本地,而是使用 FTP 存储在另一台服务器上。

我一直在努力获取this工作代码,看起来很简单,但是当我运行 python manage.py runserver

时,我不断收到 ImportError: No module named FTPStorage

我查看了多个存储库并搜索了该站点,但无济于事。我想这是一个相当简单的任务,但我似乎无法让它工作。

谢谢。

文件夹结构

设置.py

""" 
 
Django settings for myproject project. 
 
Generated by 'django-admin startproject' using Django 1.8. 
 
For more information on this file, see 
https://docs.djangoproject.com/en/1.8/topics/settings/ 
 
For the full list of settings and their values, see 
https://docs.djangoproject.com/en/1.8/ref/settings/ 
""" 
 
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) 
import os 
 
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) 
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 
 
 
# Quick-start development settings - unsuitable for production 
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 
 
# SECURITY WARNING: keep the secret key used in production secret! 
SECRET_KEY = '<The very long super secret key>' 
 
# SECURITY WARNING: don't run with debug turned on in production! 
DEBUG = True 
 
ALLOWED_HOSTS = [] 
 
 
# Application definition 
 
INSTALLED_APPS = ( 
    'django.contrib.admin', 
    'django.contrib.auth', 
    'django.contrib.contenttypes', 
    'django.contrib.sessions', 
    'django.contrib.messages', 
    'django.contrib.staticfiles', 
    'myproject.myapp', 
    'storages', 
) 
 
MIDDLEWARE_CLASSES = ( 
    'django.contrib.sessions.middleware.SessionMiddleware', 
    'django.middleware.common.CommonMiddleware', 
    'django.middleware.csrf.CsrfViewMiddleware', 
    'django.contrib.auth.middleware.AuthenticationMiddleware', 
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 
    'django.contrib.messages.middleware.MessageMiddleware', 
    'django.middleware.clickjacking.XFrameOptionsMiddleware', 
    'django.middleware.security.SecurityMiddleware', 
) 
 
ROOT_URLCONF = 'myproject.urls' 
 
TEMPLATES = [ 
    { 
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [ 
            os.path.join(BASE_DIR, 'myproject', 'myapp', 'templates') 
        ], 
        'APP_DIRS': True, 
        'OPTIONS': { 
            'context_processors': [ 
                # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this 
                # list if you haven't customized them: 
                'django.contrib.auth.context_processors.auth', 
                'django.template.context_processors.debug', 
                'django.template.context_processors.i18n', 
                'django.template.context_processors.media', 
                'django.template.context_processors.static', 
                'django.template.context_processors.tz', 
                'django.contrib.messages.context_processors.messages', 
            ], 
        }, 
    }, 
] 
 
WSGI_APPLICATION = 'myproject.wsgi.application' 
 
 
# Database 
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases 
 
DATABASES = { 
    'default': { 
        'ENGINE': 'django.db.backends.sqlite3', 
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 
    } 
} 
 
 
# Internationalization 
# https://docs.djangoproject.com/en/1.8/topics/i18n/ 
 
LANGUAGE_CODE = 'en-us' 
 
TIME_ZONE = 'UTC' 
 
USE_I18N = True 
 
USE_L10N = True 
 
USE_TZ = True 
 
 
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 
MEDIA_URL = '/media/' 
 
# Static files (CSS, JavaScript, Images) 
# https://docs.djangoproject.com/en/1.8/howto/static-files/ 
STATIC_URL = '/static/' 
 
DEFAULT_FILE_STORAGE = 'storages.backends.ftp.FTPStorage' 
FTP_STORAGE_LOCATION = 'ftp://<user>:<pass>@<host>:<port>/[path]' 

模型.py

# -*- coding: utf-8 -*- 
from django.db import models 
from FTPStorage import FTPStorage 
 
fs = FTPStorage() 
class FTPTest(models.Model): 
    file = models.FileField(upload_to='srv/ftp/', storage=fs) 
 
class Document(models.Model): 
    docfile = models.FileField(upload_to='documents') 

ftp.py

# FTP storage class for Django pluggable storage system. 
# Author: Rafal Jonca <jonca.rafal@gmail.com> 
# License: MIT 
# Comes from http://www.djangosnippets.org/snippets/1269/ 
# 
# Usage: 
# 
# Add below to settings.py: 
# FTP_STORAGE_LOCATION = '[a]ftp://<user>:<pass>@<host>:<port>/[path]' 
# 
# In models.py you can write: 
# from FTPStorage import FTPStorage 
# fs = FTPStorage() 
# class FTPTest(models.Model): 
#     file = models.FileField(upload_to='a/b/c/', storage=fs) 
 
import os 
from datetime import datetime 
import ftplib 
 
from django.conf import settings 
from django.core.files.base import File 
from django.core.exceptions import ImproperlyConfigured 
 
from storages.compat import urlparse, BytesIO, Storage 
 
 
class FTPStorageException(Exception): 
    pass 
 
 
class FTPStorage(Storage): 
    """FTP Storage class for Django pluggable storage system.""" 
 
    def __init__(self, location=settings.FTP_STORAGE_LOCATION, 
                 base_url=settings.MEDIA_URL): 
        self._config = self._decode_location(location) 
        self._base_url = base_url 
        self._connection = None 
 
    def _decode_location(self, location): 
        """Return splitted configuration data from location.""" 
        splitted_url = urlparse.urlparse(location) 
        config = {} 
 
        if splitted_url.scheme not in ('ftp', 'aftp'): 
            raise ImproperlyConfigured( 
                'FTPStorage works only with FTP protocol!' 
            ) 
        if splitted_url.hostname == '': 
            raise ImproperlyConfigured('You must at least provide hostname!') 
 
        if splitted_url.scheme == 'aftp': 
            config['active'] = True 
        else: 
            config['active'] = False 
        config['path'] = splitted_url.path 
        config['host'] = splitted_url.hostname 
        config['user'] = splitted_url.username 
        config['passwd'] = splitted_url.password 
        config['port'] = int(splitted_url.port) 
 
        return config 
 
    def _start_connection(self): 
        # Check if connection is still alive and if not, drop it. 
        if self._connection is not None: 
            try: 
                self._connection.pwd() 
            except ftplib.all_errors: 
                self._connection = None 
 
        # Real reconnect 
        if self._connection is None: 
            ftp = ftplib.FTP() 
            try: 
                ftp.connect(self._config['host'], self._config['port']) 
                ftp.login(self._config['user'], self._config['passwd']) 
                if self._config['active']: 
                    ftp.set_pasv(False) 
                if self._config['path'] != '': 
                    ftp.cwd(self._config['path']) 
                self._connection = ftp 
                return 
            except ftplib.all_errors: 
                raise FTPStorageException( 
                    'Connection or login error using data %s' 
                    % repr(self._config) 
                ) 
 
    def disconnect(self): 
        self._connection.quit() 
        self._connection = None 
 
    def _mkremdirs(self, path): 
        pwd = self._connection.pwd() 
        path_splitted = path.split('/') 
        for path_part in path_splitted: 
            try: 
                self._connection.cwd(path_part) 
            except: 
                try: 
                    self._connection.mkd(path_part) 
                    self._connection.cwd(path_part) 
                except ftplib.all_errors: 
                    raise FTPStorageException( 
                        'Cannot create directory chain %s' % path 
                    ) 
        self._connection.cwd(pwd) 
        return 
 
    def _put_file(self, name, content): 
        # Connection must be open! 
        try: 
            self._mkremdirs(os.path.dirname(name)) 
            pwd = self._connection.pwd() 
            self._connection.cwd(os.path.dirname(name)) 
            self._connection.storbinary('STOR ' + os.path.basename(name), 
                                        content.file, 
                                        content.DEFAULT_CHUNK_SIZE) 
            self._connection.cwd(pwd) 
        except ftplib.all_errors: 
            raise FTPStorageException('Error writing file %s' % name) 
 
    def _open(self, name, mode='rb'): 
        remote_file = FTPStorageFile(name, self, mode=mode) 
        return remote_file 
 
    def _read(self, name): 
        memory_file = BytesIO() 
        try: 
            pwd = self._connection.pwd() 
            self._connection.cwd(os.path.dirname(name)) 
            self._connection.retrbinary('RETR ' + os.path.basename(name), 
                                        memory_file.write) 
            self._connection.cwd(pwd) 
            return memory_file 
        except ftplib.all_errors: 
            raise FTPStorageException('Error reading file %s' % name) 
 
    def _save(self, name, content): 
        content.open() 
        self._start_connection() 
        self._put_file(name, content) 
        content.close() 
        return name 
 
    def _get_dir_details(self, path): 
        # Connection must be open! 
        try: 
            lines = [] 
            self._connection.retrlines('LIST ' + path, lines.append) 
            dirs = {} 
            files = {} 
            for line in lines: 
                words = line.split() 
                if len(words) < 6: 
                    continue 
                if words[-2] == '->': 
                    continue 
                if words[0][0] == 'd': 
                    dirs[words[-1]] = 0 
                elif words[0][0] == '-': 
                    files[words[-1]] = int(words[-5]) 
            return dirs, files 
        except ftplib.all_errors: 
            raise FTPStorageException('Error getting listing for %s' % path) 
 
    def modified_time(self, name): 
        self._start_connection() 
        resp = self._connection.sendcmd('MDTM ' + name) 
        if resp[:3] == '213': 
            s = resp[3:].strip() 
            # workaround for broken FTP servers returning responses 
            # starting with e.g. 1904... instead of 2004... 
            if len(s) == 15 and s[:2] == '19': 
                s = str(1900 + int(s[2:5])) + s[5:] 
            return datetime.strptime(s, '%Y%m%d%H%M%S') 
        raise FTPStorageException( 
                'Error getting modification time of file %s' % name 
        ) 
 
    def listdir(self, path): 
        self._start_connection() 
        try: 
            dirs, files = self._get_dir_details(path) 
            return dirs.keys(), files.keys() 
        except FTPStorageException: 
            raise 
 
    def delete(self, name): 
        if not self.exists(name): 
            return 
        self._start_connection() 
        try: 
            self._connection.delete(name) 
        except ftplib.all_errors: 
            raise FTPStorageException('Error when removing %s' % name) 
 
    def exists(self, name): 
        self._start_connection() 
        try: 
            nlst = self._connection.nlst( 
                os.path.dirname(name) + '/' 
            ) 
            if name in nlst or os.path.basename(name) in nlst: 
                return True 
            else: 
                return False 
        except ftplib.error_temp: 
            return False 
        except ftplib.error_perm: 
            # error_perm: 550 Can't find file 
            return False 
        except ftplib.all_errors: 
            raise FTPStorageException('Error when testing existence of %s' 
                                      % name) 
 
    def size(self, name): 
        self._start_connection() 
        try: 
            dirs, files = self._get_dir_details(os.path.dirname(name)) 
            if os.path.basename(name) in files: 
                return files[os.path.basename(name)] 
            else: 
                return 0 
        except FTPStorageException: 
            return 0 
 
    def url(self, name): 
        if self._base_url is None: 
            raise ValueError("This file is not accessible via a URL.") 
        return urlparse.urljoin(self._base_url, name).replace('\\', '/') 
 
 
class FTPStorageFile(File): 
    def __init__(self, name, storage, mode): 
        self.name = name 
        self._storage = storage 
        self._mode = mode 
        self._is_dirty = False 
        self.file = BytesIO() 
        self._is_read = False 
 
    @property 
    def size(self): 
        if not hasattr(self, '_size'): 
            self._size = self._storage.size(self.name) 
        return self._size 
 
    def read(self, num_bytes=None): 
        if not self._is_read: 
            self._storage._start_connection() 
            self.file = self._storage._read(self.name) 
            self._is_read = True 
 
        return self.file.read(num_bytes) 
 
    def write(self, content): 
        if 'w' not in self._mode: 
            raise AttributeError("File was opened for read-only access.") 
        self.file = BytesIO(content) 
        self._is_dirty = True 
        self._is_read = True 
 
    def close(self): 
        if self._is_dirty: 
            self._storage._start_connection() 
            self._storage._put_file(self.name, self) 
            self._storage.disconnect() 
        self.file.close() 

请您参考如下方法:

看来您的导入有误。如果文件名为 ftp.py,则导入应为:

from ftp import FTPStorage 

根据文件相对于您的 PYTHONPATH 的位置,您可能需要添加更多内容,例如:

from your_app.ftp import ...