复制InputStream,如果大小超过限制,则中止操作

22

我尝试将InputStream复制到File,并且如果InputStream的大小大于1MB,则中止复制。在Java7中,我的代码如下:

public void copy(InputStream input, Path target) {
    OutputStream out = Files.newOutputStream(target,
            StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
    boolean isExceed = false;
    try {
        long nread = 0L;
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = input.read(buf)) > 0) {
            out.write(buf, 0, n);
            nread += n;
            if (nread > 1024 * 1024) {// Exceed 1 MB
                isExceed = true;
                break;
            }
        }
    } catch (IOException ex) {
        throw ex;
    } finally {
        out.close();
        if (isExceed) {// Abort the copy
            Files.deleteIfExists(target);
            throw new IllegalArgumentException();
        }
    }}
  • 第一个问题:有没有更好的解决方案?
  • 第二个问题:我的另一个解决方案-在复制操作之前,我计算这个InputStream的大小。因此,我将InputStream复制到ByteArrayOutputStream,然后获取size()。但问题是InputStream可能不支持markSupported(),因此InputStream无法在复制文件操作中重复使用。

我会在写入之前进行测试,而不是之后。你无法“计算”InputStream的大小。它可能是无限的。这个概念是没有意义的。通过使用mark()reset()来耍花招只有在下载整个流时才能起作用,而这正是你试图避免的。 - user207421
6个回答

23

我的个人选择是一个 InputStream 包装器,它在读取字节时计算它们的数量:

public class LimitedSizeInputStream extends InputStream {

    private final InputStream original;
    private final long maxSize;
    private long total;

    public LimitedSizeInputStream(InputStream original, long maxSize) {
        this.original = original;
        this.maxSize = maxSize;
    }

    @Override
    public int read() throws IOException {
        int i = original.read();
        if (i>=0) incrementCounter(1);
        return i;
    }

    @Override
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    @Override
    public int read(byte b[], int off, int len) throws IOException {
        int i = original.read(b, off, len);
        if (i>=0) incrementCounter(i);
        return i;
    }

    private void incrementCounter(int size) throws IOException {
        total += size;
        if (total>maxSize) throw new IOException("InputStream exceeded maximum size in bytes.");
    }

}

我喜欢这种方法,因为它透明易懂,在所有输入流中都可以重复使用,并且与其他库很好地配合使用。例如,在Apache Commons中复制文件高达4KB:

InputStream in = new LimitedSizeInputStream(new FileInputStream("from.txt"), 4096);
OutputStream out = new FileOutputStream("to.txt");
IOUtils.copy(in, out);

PS: 上述实现与BoundedInputStream的主要区别在于,当超过限制时,BoundedInputStream不会抛出异常(仅关闭流)


2
对于简单的文件上传情况:if (IOUtils.copy(new BoundedInputStream(inputStream, MAX_SIZE + 1), new FileOutputStream(tmpFile)) > MAX_SIZE) { /* error */} - btpka3
这种方法很有道理,但为什么在read()方法中要将incrementCounter(1)增加1?它不应该按i递增吗?除非original.read()总是返回1?我不确定是否可以假设这一点。另外,似乎你缺少close()的重写,否则你包装的流可能永远不会被处理。感谢您的回答。 - Ruben Daddario
1
考虑扩展FilterInputStream而不是InputStream,因为这是实现类型为InputStream的装饰器的推荐方法。 - Nicolas Filotto

17

4
上传文件(uploadFile):限制字节流大小为maxSize(ByteStreams.limit(stream, maxSize))。 // TODO 如何判断是否达到了maxSize大小的限制? - RvPr
太好了,正是我想要的!何必再造轮子呢? - JohnyTex

3
第一个问题:有更好的解决方案吗?
实际上没有。当然,也不会明显更好。
第二个问题 - 我的另一个解决方案 - 在复制操作之前,我计算InputStream的大小。因此,我将InputStream复制到ByteArrayOutputStream中,然后获取size()。但问题是InputStream可能不支持标记(markSupported()),所以在复制文件操作时无法重复使用InputStream。
除了上面是陈述而不是问题之外...
如果您已将字节复制到ByteArrayOutputStream中,则可以从baos.toByteArray()返回的字节数组创建ByteArrayInputStream。因此,您不需要标记/重置原始流。
但是,这是一种相当丑陋的实现方式。最重要的是,您仍然在读取和缓冲整个输入流。

谢谢!你的意思是你同意我的 copy() 解决方案? - 卢声远 Shengyuan Lu
2
如果调用该方法的大多数结果为中止,则可以考虑将最多1MB数据读入缓冲区,只有在输入数据不太大的情况下才创建输出文件。但我怀疑这种情况是否会发生。 - Stephen C

2
这是来自Apache Tomcat的实现:

最初的回答:

package org.apache.tomcat.util.http.fileupload.util;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * An input stream, which limits its data size. This stream is
 * used, if the content length is unknown.
 */
public abstract class LimitedInputStream extends FilterInputStream implements Closeable {

    /**
     * The maximum size of an item, in bytes.
     */
    private final long sizeMax;

    /**
     * The current number of bytes.
     */
    private long count;

    /**
     * Whether this stream is already closed.
     */
    private boolean closed;

    /**
     * Creates a new instance.
     *
     * @param inputStream The input stream, which shall be limited.
     * @param pSizeMax The limit; no more than this number of bytes
     *   shall be returned by the source stream.
     */
    public LimitedInputStream(InputStream inputStream, long pSizeMax) {
        super(inputStream);
        sizeMax = pSizeMax;
    }

    /**
     * Called to indicate, that the input streams limit has
     * been exceeded.
     *
     * @param pSizeMax The input streams limit, in bytes.
     * @param pCount The actual number of bytes.
     * @throws IOException The called method is expected
     *   to raise an IOException.
     */
    protected abstract void raiseError(long pSizeMax, long pCount)
            throws IOException;

    /**
     * Called to check, whether the input streams
     * limit is reached.
     *
     * @throws IOException The given limit is exceeded.
     */
    private void checkLimit() throws IOException {
        if (count > sizeMax) {
            raiseError(sizeMax, count);
        }
    }

    /**
     * Reads the next byte of data from this input stream. The value
     * byte is returned as an <code>int</code> in the range
     * <code>0</code> to <code>255</code>. If no byte is available
     * because the end of the stream has been reached, the value
     * <code>-1</code> is returned. This method blocks until input data
     * is available, the end of the stream is detected, or an exception
     * is thrown.
     * <p>
     * This method
     * simply performs <code>in.read()</code> and returns the result.
     *
     * @return     the next byte of data, or <code>-1</code> if the end of the
     *             stream is reached.
     * @throws  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    @Override
    public int read() throws IOException {
        int res = super.read();
        if (res != -1) {
            count++;
            checkLimit();
        }
        return res;
    }

    /**
     * Reads up to <code>len</code> bytes of data from this input stream
     * into an array of bytes. If <code>len</code> is not zero, the method
     * blocks until some input is available; otherwise, no
     * bytes are read and <code>0</code> is returned.
     * <p>
     * This method simply performs <code>in.read(b, off, len)</code>
     * and returns the result.
     *
     * @param      b     the buffer into which the data is read.
     * @param      off   The start offset in the destination array
     *                   <code>b</code>.
     * @param      len   the maximum number of bytes read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> if there is no more data because the end of
     *             the stream has been reached.
     * @throws  NullPointerException If <code>b</code> is <code>null</code>.
     * @throws  IndexOutOfBoundsException If <code>off</code> is negative,
     * <code>len</code> is negative, or <code>len</code> is greater than
     * <code>b.length - off</code>
     * @throws  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int res = super.read(b, off, len);
        if (res > 0) {
            count += res;
            checkLimit();
        }
        return res;
    }

    /**
     * Returns, whether this stream is already closed.
     *
     * @return True, if the stream is closed, otherwise false.
     * @throws IOException An I/O error occurred.
     */
    @Override
    public boolean isClosed() throws IOException {
        return closed;
    }

    /**
     * Closes this input stream and releases any system resources
     * associated with the stream.
     * This
     * method simply performs <code>in.close()</code>.
     *
     * @throws  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    @Override
    public void close() throws IOException {
        closed = true;
        super.close();
    }
}

您需要创建子类并重写raiseError方法。


1
更方便快捷的解决方案,用于检查输入流的大小。
    FileChannel chanel = (FileChannel) Channels.newChannel(inputStream);
    MappedByteBuffer buffer = chanel.map(FileChannel.MapMode.READ_ONLY, 0, chanel.size());

    System.out.println(buffer.capacity()); // bytes

0

我喜欢基于ByteArrayOutputStream的解决方案,我不明白为什么它不能工作

public void copy(InputStream input, Path target) throws IOException {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    BufferedInputStream bis = new BufferedInputStream(input);
    for (int b = 0; (b = bis.read()) != -1;) {
        if (bos.size() > BUFFER_SIZE) {
            throw new IOException();
        }
        bos.write(b);
    }
    Files.write(target, bos.toByteArray());
}

当然,它可以工作。我只是质疑像那样在内存中缓冲文件的效用。除非我们假设“太大”的情况将占很大比例,否则直接写入输出文件并在出错情况下像OP一样删除它更便宜、更简单。 - Stephen C
点赞。我的担忧是,将来限制会更高,因此 ByteArrayOutputStream 将消耗大量内存。 - 卢声远 Shengyuan Lu
为什么浪费那么多的内存? - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接