View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.stream;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.buffer.ByteBufAllocator;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.channel.FileRegion;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.RandomAccessFile;
26  
27  /**
28   * A {@link ChunkedInput} that fetches data from a file chunk by chunk.
29   * <p>
30   * If your operating system supports
31   * <a href="http://en.wikipedia.org/wiki/Zero-copy">zero-copy file transfer</a>
32   * such as {@code sendfile()}, you might want to use {@link FileRegion} instead.
33   */
34  public class ChunkedFile implements ChunkedInput<ByteBuf> {
35  
36      private final RandomAccessFile file;
37      private final long startOffset;
38      private final long endOffset;
39      private final int chunkSize;
40      private long offset;
41  
42      /**
43       * Creates a new instance that fetches data from the specified file.
44       */
45      public ChunkedFile(File file) throws IOException {
46          this(file, ChunkedStream.DEFAULT_CHUNK_SIZE);
47      }
48  
49      /**
50       * Creates a new instance that fetches data from the specified file.
51       *
52       * @param chunkSize the number of bytes to fetch on each
53       *                  {@link #readChunk(ChannelHandlerContext)} call
54       */
55      public ChunkedFile(File file, int chunkSize) throws IOException {
56          this(new RandomAccessFile(file, "r"), chunkSize);
57      }
58  
59      /**
60       * Creates a new instance that fetches data from the specified file.
61       */
62      public ChunkedFile(RandomAccessFile file) throws IOException {
63          this(file, ChunkedStream.DEFAULT_CHUNK_SIZE);
64      }
65  
66      /**
67       * Creates a new instance that fetches data from the specified file.
68       *
69       * @param chunkSize the number of bytes to fetch on each
70       *                  {@link #readChunk(ChannelHandlerContext)} call
71       */
72      public ChunkedFile(RandomAccessFile file, int chunkSize) throws IOException {
73          this(file, 0, file.length(), chunkSize);
74      }
75  
76      /**
77       * Creates a new instance that fetches data from the specified file.
78       *
79       * @param offset the offset of the file where the transfer begins
80       * @param length the number of bytes to transfer
81       * @param chunkSize the number of bytes to fetch on each
82       *                  {@link #readChunk(ChannelHandlerContext)} call
83       */
84      public ChunkedFile(RandomAccessFile file, long offset, long length, int chunkSize) throws IOException {
85          if (file == null) {
86              throw new NullPointerException("file");
87          }
88          if (offset < 0) {
89              throw new IllegalArgumentException(
90                      "offset: " + offset + " (expected: 0 or greater)");
91          }
92          if (length < 0) {
93              throw new IllegalArgumentException(
94                      "length: " + length + " (expected: 0 or greater)");
95          }
96          if (chunkSize <= 0) {
97              throw new IllegalArgumentException(
98                      "chunkSize: " + chunkSize +
99                      " (expected: a positive integer)");
100         }
101 
102         this.file = file;
103         this.offset = startOffset = offset;
104         endOffset = offset + length;
105         this.chunkSize = chunkSize;
106 
107         file.seek(offset);
108     }
109 
110     /**
111      * Returns the offset in the file where the transfer began.
112      */
113     public long startOffset() {
114         return startOffset;
115     }
116 
117     /**
118      * Returns the offset in the file where the transfer will end.
119      */
120     public long endOffset() {
121         return endOffset;
122     }
123 
124     /**
125      * Returns the offset in the file where the transfer is happening currently.
126      */
127     public long currentOffset() {
128         return offset;
129     }
130 
131     @Override
132     public boolean isEndOfInput() throws Exception {
133         return !(offset < endOffset && file.getChannel().isOpen());
134     }
135 
136     @Override
137     public void close() throws Exception {
138         file.close();
139     }
140 
141     @Deprecated
142     @Override
143     public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception {
144         return readChunk(ctx.alloc());
145     }
146 
147     @Override
148     public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception {
149         long offset = this.offset;
150         if (offset >= endOffset) {
151             return null;
152         }
153 
154         int chunkSize = (int) Math.min(this.chunkSize, endOffset - offset);
155         // Check if the buffer is backed by an byte array. If so we can optimize it a bit an safe a copy
156 
157         ByteBuf buf = allocator.heapBuffer(chunkSize);
158         boolean release = true;
159         try {
160             file.readFully(buf.array(), buf.arrayOffset(), chunkSize);
161             buf.writerIndex(chunkSize);
162             this.offset = offset + chunkSize;
163             release = false;
164             return buf;
165         } finally {
166             if (release) {
167                 buf.release();
168             }
169         }
170     }
171 
172     @Override
173     public long length() {
174         return endOffset - startOffset;
175     }
176 
177     @Override
178     public long progress() {
179         return offset - startOffset;
180     }
181 }