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    *   https://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  import io.netty.util.internal.ObjectUtil;
23  
24  import java.io.EOFException;
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.RandomAccessFile;
28  import java.nio.channels.ClosedChannelException;
29  import java.nio.channels.FileChannel;
30  
31  /**
32   * A {@link ChunkedInput} that fetches data from a file chunk by chunk using
33   * NIO {@link FileChannel}.
34   * <p>
35   * If your operating system supports
36   * <a href="https://en.wikipedia.org/wiki/Zero-copy">zero-copy file transfer</a>
37   * such as {@code sendfile()}, you might want to use {@link FileRegion} instead.
38   */
39  public class ChunkedNioFile implements ChunkedInput<ByteBuf> {
40  
41      private final FileChannel in;
42      private final long startOffset;
43      private final long endOffset;
44      private final int chunkSize;
45      private long offset;
46  
47      /**
48       * Creates a new instance that fetches data from the specified file.
49       */
50      public ChunkedNioFile(File in) throws IOException {
51          this(new RandomAccessFile(in, "r").getChannel());
52      }
53  
54      /**
55       * Creates a new instance that fetches data from the specified file.
56       *
57       * @param chunkSize the number of bytes to fetch on each
58       *                  {@link #readChunk(ChannelHandlerContext)} call
59       */
60      public ChunkedNioFile(File in, int chunkSize) throws IOException {
61          this(new RandomAccessFile(in, "r").getChannel(), chunkSize);
62      }
63  
64      /**
65       * Creates a new instance that fetches data from the specified file.
66       */
67      public ChunkedNioFile(FileChannel in) throws IOException {
68          this(in, ChunkedStream.DEFAULT_CHUNK_SIZE);
69      }
70  
71      /**
72       * Creates a new instance that fetches data from the specified file.
73       *
74       * @param chunkSize the number of bytes to fetch on each
75       *                  {@link #readChunk(ChannelHandlerContext)} call
76       */
77      public ChunkedNioFile(FileChannel in, int chunkSize) throws IOException {
78          this(in, 0, in.size(), chunkSize);
79      }
80  
81      /**
82       * Creates a new instance that fetches data from the specified file.
83       *
84       * @param offset the offset of the file where the transfer begins
85       * @param length the number of bytes to transfer
86       * @param chunkSize the number of bytes to fetch on each
87       *                  {@link #readChunk(ChannelHandlerContext)} call
88       */
89      public ChunkedNioFile(FileChannel in, long offset, long length, int chunkSize)
90              throws IOException {
91          ObjectUtil.checkNotNull(in, "in");
92          ObjectUtil.checkPositiveOrZero(offset, "offset");
93          ObjectUtil.checkPositiveOrZero(length, "length");
94          ObjectUtil.checkPositive(chunkSize, "chunkSize");
95          if (!in.isOpen()) {
96              throw new ClosedChannelException();
97          }
98          this.in = in;
99          this.chunkSize = chunkSize;
100         this.offset = startOffset = offset;
101         endOffset = offset + length;
102     }
103 
104     /**
105      * Returns the offset in the file where the transfer began.
106      */
107     public long startOffset() {
108         return startOffset;
109     }
110 
111     /**
112      * Returns the offset in the file where the transfer will end.
113      */
114     public long endOffset() {
115         return endOffset;
116     }
117 
118     /**
119      * Returns the offset in the file where the transfer is happening currently.
120      */
121     public long currentOffset() {
122         return offset;
123     }
124 
125     @Override
126     public boolean isEndOfInput() throws Exception {
127         return !(offset < endOffset && in.isOpen());
128     }
129 
130     @Override
131     public void close() throws Exception {
132         in.close();
133     }
134 
135     @Deprecated
136     @Override
137     public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception {
138         return readChunk(ctx.alloc());
139     }
140 
141     @Override
142     public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception {
143         long offset = this.offset;
144         if (offset >= endOffset) {
145             return null;
146         }
147 
148         int chunkSize = (int) Math.min(this.chunkSize, endOffset - offset);
149         ByteBuf buffer = allocator.buffer(chunkSize);
150         boolean release = true;
151         try {
152             int readBytes = 0;
153             for (;;) {
154                 int localReadBytes = buffer.writeBytes(in, offset + readBytes, chunkSize - readBytes);
155                 if (localReadBytes < 0) {
156                     // The file is shorter than the region we were asked to transfer (for example it was truncated
157                     // after this ChunkedNioFile was created). Fail like ChunkedFile instead of returning empty
158                     // chunks forever, as isEndOfInput() would never return true.
159                     throw new EOFException("Underlying file size " + in.size() + " smaller than requested end " +
160                             "offset " + endOffset);
161                 }
162                 readBytes += localReadBytes;
163                 if (readBytes == chunkSize) {
164                     break;
165                 }
166             }
167             this.offset += readBytes;
168             release = false;
169             return buffer;
170         } finally {
171             if (release) {
172                 buffer.release();
173             }
174         }
175     }
176 
177     @Override
178     public long length() {
179         return endOffset - startOffset;
180     }
181 
182     @Override
183     public long progress() {
184         return offset - startOffset;
185     }
186 }