【问题标题】:Limiting file size in Twisted FTP server限制 Twisted FTP 服务器中的文件大小
【发布时间】:2017-03-20 17:23:44
【问题描述】:

我正在尝试使用限制上传文件大小的扭曲实现 FTP 服务器。理想情况下,这会在传输开始之前发生,但如果它太大,如果它在传输过程中优雅退出,这并不是真正的问题。

我从非常基本的 ftpserver.py 开始,慢慢地从 ftp.py 中引入更多底层类以深入了解内部结构。

下面的当前代码,请原谅使用的“hack-and-slash”样式,直到我可以让它工作为止。

#!/usr/bin/python
import os

from twisted.protocols.ftp import FTPFactory, FTPShell, FTPAnonymousShell, IFTPShell
from twisted.cred.portal import Portal
from twisted.cred.checkers import AllowAnonymousAccess
from twisted.internet import reactor, defer
from twisted.python import filepath, failure

class FileConsumer1(object):
    def __init__(self, fObj):
        self.fObj = fObj

    def registerProducer(self, producer, streaming):
        self.producer = producer
        assert streaming

    def unregisterProducer(self):
        self.producer = None
        self.fObj.close()

    def write(self, bytes):
        size = os.fstat(self.fObj.fileno()).st_size + len(bytes)
        if size > 10:
            raise Exception("File too large") # WHAT GOES HERE?
        self.fObj.write(bytes)

class FileWriter1(object):
    def __init__(self, fObj):
        self.fObj = fObj
        self._receive = False

    def receive(self):
        assert not self._receive, "Can only call IWriteFile.receive *once* per instance"
        self._receive = True
        return defer.succeed(FileConsumer1(self.fObj))

    def close(self):
        return defer.succeed(None)

class FTPShell1(FTPShell):
    def openForWriting(self, path):

        p = self._path(path)
        if p.isdir():
            return defer.fail(IsADirectoryError(path))
        try:
            fObj = p.open('w')
        except (IOError, OSError), e:
            return errnoToFailure(e.errno, path)
        except:
            return defer.fail()
        return defer.succeed(FileWriter1(fObj))

class FTPRealm1(object):
    def __init__(self, root):
        self.path = filepath.FilePath(root)

    def requestAvatar(self, avatarId, mind, *interfaces):
        avatar = FTPShell1(self.path)
        return (IFTPShell, avatar, getattr(avatar, 'logout', lambda: None))

p = Portal(FTPRealm1('./'), [ AllowAnonymousAccess() ])

f = FTPFactory(p)

reactor.listenTCP(4021, f)
reactor.run()

显然,如果 size > 10 的检查会更大,但是此时应该如何指示存在问题?就目前而言,twisted 捕获了该异常,但它不是很优雅。据我从对 ftp.py 的检查中可以看出,没有什么明显的东西可以返回这里。我可以以某种方式传递延期吗?我应该如何优雅地关闭传输?

谢谢,

这是修改后的版本

#!/usr/bin/python
import os

from zope.interface import Interface, implements

from twisted.protocols.ftp import FTPFactory, FTPShell, FTPAnonymousShell, IFTPShell, IWriteFile    , BaseFTPRealm, FTPCmdError, EXCEEDED_STORAGE_ALLOC
from twisted.cred.portal import Portal
from twisted.cred.checkers import AllowAnonymousAccess
from twisted.internet import reactor, defer, interfaces
from twisted.python import filepath

class ExceededStorageAllocError(FTPCmdError):
    errorCode = EXCEEDED_STORAGE_ALLOC

class FileConsumer(object):
    implements(interfaces.IConsumer)
    def __init__(self):
        self.data = ""
        self.error = None

    def registerProducer(self, producer, streaming):
        self.producer = producer
        assert streaming

    def unregisterProducer(self):
        if self.producer:
            self.producer.stopProducing()
        self.producer = None

    def write(self, bytes):
        self.data += bytes
        if len(self.data) > 10:
            self.unregisterProducer()
            self.error = ExceededStorageAllocError()

class FileWriter(object):
    implements(IWriteFile)
    def __init__(self, path):
        self.path = path

    def receive(self):
        self.consumer = FileConsumer()
        return defer.succeed(self.consumer)

    def close(self):
        if self.consumer.error:
            return defer.fail(self.consumer.error)
        try:
            f = self.path.open('w')
        except (IOError, OSError), e:
            return errnoToFailure(e.errno, path)
        f.write(self.consumer.data)
        return defer.succeed(None)

class FTPShell1(FTPShell):
    makeDirectory = FTPAnonymousShell.makeDirectory
    removeDirectory = FTPAnonymousShell.removeDirectory
    def openForWriting(self, path):
        p = self._path(path)
        if p.isdir():
            return defer.fail(IsADirectoryError(path))
        return defer.succeed(FileWriter(p))

class FTPRealm1(BaseFTPRealm):
    def __init__(self, root):
        self.root = root

    def requestAvatar(self, avatarId, mind, *interfaces):
        avatar = FTPShell1(filepath.FilePath(self.root))
        return (IFTPShell, avatar, getattr(avatar, 'logout', lambda: None))

p = Portal(FTPRealm1('./'), [ AllowAnonymousAccess() ])

f = FTPFactory(p)

reactor.listenTCP(4021, f)
reactor.run()

它在 FileConsumer() 中累积接收到的数据,然后如果文件太长则中止。 FileWriter() 的 close() 方法然后要么报告错误,要么将完整的缓冲区写入文件。

我遇到的唯一真正问题是,在运行时,异常会显示在服务器上:

Unexpected error received during transfer:
Traceback (most recent call last):
Failure: __main__.ExceededStorageAllocError: 

【问题讨论】:

    标签: python ftp twisted


    【解决方案1】:

    作为一个快速免责声明,我对 Twisted 的生产者/消费者模型非常不满意,所以这可能行不通。与往常一样,如果事情发生爆炸,我概不负责;)

    您似乎走在正确的道路上,所以请为此拍拍自己的后背。我认为如果您在文件太大时调用unregisterProducer,则该文件应该停止使用。您可能还需要致电self.producer.stopProducing(),但请不要引用我的话。

    def unregisterProducer(self):
        self.producer.stopProducing()
        self.fObj.close()
    
    def write(self, bytes):
        size = os.fstat(self.fObj.fileno()).st_size + len(bytes)
        if size > 10:
            self.unregisterConsumer()
            # log statements would go here
            # do some clean up too
        self.fObj.write(bytes)
    

    如果我的心理代码 Python 解释器是正确的,这应该只是停止使用该文件。至于您应该返回给客户端的内容,您将不得不阅读有关 FTP 的 RFC 才能弄清楚。

    附言

    尽管看起来很乏味,但请使用 @implementor 装饰器。大多数情况下你会没事的,但有时可能会出现意外的回溯。

    【讨论】:

    • 您好,感谢您的想法,虽然我已经使用twisted有一段时间了,但这是我第一次接触生产者/消费者模型。我试过你的建议, FileConsumer1() 类没有 unregisterCosumer() 方法 - 你的意思是 unregisterProducer() 吗?
    • 哎呀,是的,我的意思是unregisterProducer。也编辑了答案。
    • 查看我的第二个版本以使用您的建议进行修改,该建议运行良好,并且在请求时肯定会停止传输。
    • 要处理意外错误,您可以通过将 errbacks 链接到 defer.succeed(self.consumer) 来捕获异常。我真的应该尝试实际运行您的代码;)不幸的是,这些天我没有世界上最多的时间
    最近更新 更多