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