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.File;
25 import java.io.IOException;
26 import java.io.RandomAccessFile;
27
28 /**
29 * A {@link ChunkedInput} that fetches data from a file chunk by chunk.
30 * <p>
31 * If your operating system supports
32 * <a href="https://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 ChunkedFile implements ChunkedInput<ByteBuf> {
36
37 private final RandomAccessFile file;
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 ChunkedFile(File file) throws IOException {
47 this(file, ChunkedStream.DEFAULT_CHUNK_SIZE);
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 ChunkedFile(File file, int chunkSize) throws IOException {
57 this(new RandomAccessFile(file, "r"), chunkSize);
58 }
59
60 /**
61 * Creates a new instance that fetches data from the specified file.
62 */
63 public ChunkedFile(RandomAccessFile file) throws IOException {
64 this(file, 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 ChunkedFile(RandomAccessFile file, int chunkSize) throws IOException {
74 this(file, 0, file.length(), 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 ChunkedFile(RandomAccessFile file, long offset, long length, int chunkSize) throws IOException {
86 ObjectUtil.checkNotNull(file, "file");
87 ObjectUtil.checkPositiveOrZero(offset, "offset");
88 ObjectUtil.checkPositiveOrZero(length, "length");
89 ObjectUtil.checkPositive(chunkSize, "chunkSize");
90
91 this.file = file;
92 this.offset = startOffset = offset;
93 this.endOffset = offset + length;
94 this.chunkSize = chunkSize;
95
96 file.seek(offset);
97 }
98
99 /**
100 * Returns the offset in the file where the transfer began.
101 */
102 public long startOffset() {
103 return startOffset;
104 }
105
106 /**
107 * Returns the offset in the file where the transfer will end.
108 */
109 public long endOffset() {
110 return endOffset;
111 }
112
113 /**
114 * Returns the offset in the file where the transfer is happening currently.
115 */
116 public long currentOffset() {
117 return offset;
118 }
119
120 @Override
121 public boolean isEndOfInput() throws Exception {
122 return !(offset < endOffset && file.getChannel().isOpen());
123 }
124
125 @Override
126 public void close() throws Exception {
127 file.close();
128 }
129
130 @Deprecated
131 @Override
132 public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception {
133 return readChunk(ctx.alloc());
134 }
135
136 @Override
137 public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception {
138 long offset = this.offset;
139 if (offset >= endOffset) {
140 return null;
141 }
142
143 int chunkSize = (int) Math.min(this.chunkSize, endOffset - offset);
144 // Check if the buffer is backed by an byte array. If so we can optimize it a bit an safe a copy
145
146 ByteBuf buf = allocator.heapBuffer(chunkSize);
147 boolean release = true;
148 try {
149 file.readFully(buf.array(), buf.arrayOffset(), chunkSize);
150 buf.writerIndex(chunkSize);
151 this.offset = offset + chunkSize;
152 release = false;
153 return buf;
154 } finally {
155 if (release) {
156 buf.release();
157 }
158 }
159 }
160
161 @Override
162 public long length() {
163 return endOffset - startOffset;
164 }
165
166 @Override
167 public long progress() {
168 return offset - startOffset;
169 }
170 }