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.channel.ChannelHandlerContext;
20  import io.netty.channel.FileRegion;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.RandomAccessFile;
25  
26  /**
27   * A {@link ChunkedInput} that fetches data from a file chunk by chunk.
28   * <p>
29   * If your operating system supports
30   * <a href="http://en.wikipedia.org/wiki/Zero-copy">zero-copy file transfer</a>
31   * such as {@code sendfile()}, you might want to use {@link FileRegion} instead.
32   */
33  public class ChunkedFile implements ChunkedInput<ByteBuf> {
34  
35      private final RandomAccessFile file;
36      private final long startOffset;
37      private final long endOffset;
38      private final int chunkSize;
39      private long offset;
40  
41      /**
42       * Creates a new instance that fetches data from the specified file.
43       */
44      public ChunkedFile(File file) throws IOException {
45          this(file, ChunkedStream.DEFAULT_CHUNK_SIZE);
46      }
47  
48      /**
49       * Creates a new instance that fetches data from the specified file.
50       *
51       * @param chunkSize the number of bytes to fetch on each
52       *                  {@link #readChunk(ChannelHandlerContext)} call
53       */
54      public ChunkedFile(File file, int chunkSize) throws IOException {
55          this(new RandomAccessFile(file, "r"), chunkSize);
56      }
57  
58      /**
59       * Creates a new instance that fetches data from the specified file.
60       */
61      public ChunkedFile(RandomAccessFile file) throws IOException {
62          this(file, ChunkedStream.DEFAULT_CHUNK_SIZE);
63      }
64  
65      /**
66       * Creates a new instance that fetches data from the specified file.
67       *
68       * @param chunkSize the number of bytes to fetch on each
69       *                  {@link #readChunk(ChannelHandlerContext)} call
70       */
71      public ChunkedFile(RandomAccessFile file, int chunkSize) throws IOException {
72          this(file, 0, file.length(), chunkSize);
73      }
74  
75      /**
76       * Creates a new instance that fetches data from the specified file.
77       *
78       * @param offset the offset of the file where the transfer begins
79       * @param length the number of bytes to transfer
80       * @param chunkSize the number of bytes to fetch on each
81       *                  {@link #readChunk(ChannelHandlerContext)} call
82       */
83      public ChunkedFile(RandomAccessFile file, long offset, long length, int chunkSize) throws IOException {
84          if (file == null) {
85              throw new NullPointerException("file");
86          }
87          if (offset < 0) {
88              throw new IllegalArgumentException(
89                      "offset: " + offset + " (expected: 0 or greater)");
90          }
91          if (length < 0) {
92              throw new IllegalArgumentException(
93                      "length: " + length + " (expected: 0 or greater)");
94          }
95          if (chunkSize <= 0) {
96              throw new IllegalArgumentException(
97                      "chunkSize: " + chunkSize +
98                      " (expected: a positive integer)");
99          }
100 
101         this.file = file;
102         this.offset = startOffset = offset;
103         endOffset = offset + length;
104         this.chunkSize = chunkSize;
105 
106         file.seek(offset);
107     }
108 
109     /**
110      * Returns the offset in the file where the transfer began.
111      */
112     public long startOffset() {
113         return startOffset;
114     }
115 
116     /**
117      * Returns the offset in the file where the transfer will end.
118      */
119     public long endOffset() {
120         return endOffset;
121     }
122 
123     /**
124      * Returns the offset in the file where the transfer is happening currently.
125      */
126     public long currentOffset() {
127         return offset;
128     }
129 
130     @Override
131     public boolean isEndOfInput() throws Exception {
132         return !(offset < endOffset && file.getChannel().isOpen());
133     }
134 
135     @Override
136     public void close() throws Exception {
137         file.close();
138     }
139 
140     @Override
141     public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception {
142         long offset = this.offset;
143         if (offset >= endOffset) {
144             return null;
145         }
146 
147         int chunkSize = (int) Math.min(this.chunkSize, endOffset - offset);
148         // Check if the buffer is backed by an byte array. If so we can optimize it a bit an safe a copy
149 
150         ByteBuf buf = ctx.alloc().heapBuffer(chunkSize);
151         boolean release = true;
152         try {
153             file.readFully(buf.array(), buf.arrayOffset(), chunkSize);
154             buf.writerIndex(chunkSize);
155             this.offset = offset + chunkSize;
156             release = false;
157             return buf;
158         } finally {
159             if (release) {
160                 buf.release();
161             }
162         }
163     }
164 }