1+ // The Sisk Framework source code
2+ // Copyright (c) 2024- PROJECT PRINCIPIUM and all Sisk contributors
3+ //
4+ // The code below is licensed under the MIT license as
5+ // of the date of its publication, available at
6+ //
7+ // File name: HttpFileRangedContentStream.cs
8+ // Repository: https://github.com/sisk-http/core
9+
10+ using System . Buffers ;
11+ using System . Globalization ;
12+ using System . Net ;
13+ using Sisk . Core . Helpers ;
14+ using Sisk . Core . Http ;
15+ using Sisk . Core . Http . FileSystem ;
16+
17+ abstract class HttpFileRangedContentStream : HttpFileServerFileConverter {
18+
19+ public virtual int ChunkSize => 5 * ( int ) SizeHelper . UnitMb ;
20+
21+ public override HttpResponse Convert ( FileInfo file , HttpRequest request ) {
22+
23+ ArgumentOutOfRangeException . ThrowIfNegativeOrZero ( ChunkSize ) ;
24+
25+ using var fs = file . OpenRead ( ) ;
26+ long length = fs . Length ;
27+
28+ string ? rangeHeader = request . Headers . Range ;
29+
30+ bool hasRange = TryGetSingleRange (
31+ rangeHeader ,
32+ length ,
33+ out long rangeStart ,
34+ out long rangeEnd ,
35+ out bool isRangeSatisfiable
36+ ) ;
37+
38+ if ( hasRange && ! isRangeSatisfiable ) {
39+ return new HttpResponse ( HttpStatusCode . RequestedRangeNotSatisfiable )
40+ . WithHeader ( HttpKnownHeaderNames . ContentRange , $ "bytes */{ length } " ) ;
41+ }
42+
43+ if ( ! hasRange ) {
44+ var resStream = request . GetResponseStream ( ) ;
45+
46+ resStream . SetStatus ( HttpStatusCode . OK ) ;
47+ resStream . SetHeader ( HttpKnownHeaderNames . AcceptRanges , "bytes" ) ;
48+ resStream . SetHeader ( HttpKnownHeaderNames . ContentType , MimeHelper . GetMimeType ( file . Extension ) ) ;
49+ resStream . SetContentLength ( length ) ;
50+
51+ var buffer = ArrayPool < byte > . Shared . Rent ( ChunkSize ) ;
52+ try {
53+ int read ;
54+ while ( ( read = fs . Read ( buffer , 0 , buffer . Length ) ) > 0 ) {
55+ resStream . ResponseStream . Write ( buffer , 0 , read ) ;
56+ }
57+ }
58+ finally {
59+ ArrayPool < byte > . Shared . Return ( buffer ) ;
60+ }
61+
62+ return resStream . Close ( ) ;
63+ }
64+
65+ long requestedLength = rangeEnd - rangeStart + 1 ;
66+
67+ long maxLength = ChunkSize > 0
68+ ? Math . Min ( requestedLength , ChunkSize )
69+ : requestedLength ;
70+
71+ long actualEnd = rangeStart + maxLength - 1 ;
72+
73+ var partialResStream = request . GetResponseStream ( ) ;
74+
75+ partialResStream . SetStatus ( HttpStatusCode . PartialContent ) ;
76+ partialResStream . SetHeader ( HttpKnownHeaderNames . AcceptRanges , "bytes" ) ;
77+ partialResStream . SetHeader ( HttpKnownHeaderNames . ContentType , MimeHelper . GetMimeType ( file . Extension ) ) ;
78+ partialResStream . SetHeader (
79+ HttpKnownHeaderNames . ContentRange ,
80+ $ "bytes { rangeStart } -{ actualEnd } /{ length } "
81+ ) ;
82+ partialResStream . SetContentLength ( maxLength ) ;
83+
84+ fs . Position = rangeStart ;
85+
86+ // Buffer de leitura (no máximo ChunkSize, e nunca mais do que o que falta enviar)
87+ var rangeBuffer = ArrayPool < byte > . Shared . Rent ( ( int ) Math . Min ( ChunkSize , maxLength ) ) ;
88+ try {
89+ long remaining = maxLength ;
90+ while ( remaining > 0 ) {
91+ int read = fs . Read ( rangeBuffer , 0 , ( int ) Math . Min ( rangeBuffer . Length , remaining ) ) ;
92+ if ( read <= 0 )
93+ break ;
94+
95+ if ( request . Method != HttpMethod . Head )
96+ partialResStream . ResponseStream . Write ( rangeBuffer , 0 , read ) ;
97+ remaining -= read ;
98+ }
99+ }
100+ finally {
101+ ArrayPool < byte > . Shared . Return ( rangeBuffer ) ;
102+ }
103+
104+ return partialResStream . Close ( ) ;
105+ }
106+
107+ private static bool TryGetSingleRange ( string ? rangeHeader , long entityLength , out long rangeStart , out long rangeEnd , out bool isSatisfiable ) {
108+ rangeStart = 0 ;
109+ rangeEnd = 0 ;
110+ isSatisfiable = false ;
111+
112+ if ( string . IsNullOrWhiteSpace ( rangeHeader ) )
113+ return false ;
114+
115+ rangeHeader = rangeHeader . Trim ( ) ;
116+
117+ if ( ! rangeHeader . StartsWith ( "bytes=" , StringComparison . OrdinalIgnoreCase ) )
118+ return false ;
119+
120+ var rangeSpec = rangeHeader . Substring ( "bytes=" . Length ) . Trim ( ) ;
121+ if ( string . IsNullOrEmpty ( rangeSpec ) )
122+ return false ;
123+
124+ if ( rangeSpec . Contains ( ',' ) )
125+ return false ;
126+
127+ var parts = rangeSpec . Split ( '-' , 2 ) ;
128+ if ( parts . Length != 2 )
129+ return false ;
130+
131+ string startPart = parts [ 0 ] . Trim ( ) ;
132+ string endPart = parts [ 1 ] . Trim ( ) ;
133+
134+ // Caso 1: bytes=start-end
135+ if ( startPart . Length > 0 && endPart . Length > 0 ) {
136+ if ( ! long . TryParse ( startPart , NumberStyles . None , CultureInfo . InvariantCulture , out var start ) ||
137+ ! long . TryParse ( endPart , NumberStyles . None , CultureInfo . InvariantCulture , out var end ) ) {
138+ return false ;
139+ }
140+
141+ if ( start < 0 || end < start )
142+ return false ;
143+
144+ if ( start >= entityLength ) {
145+ isSatisfiable = false ;
146+ return true ;
147+ }
148+
149+ rangeStart = start ;
150+ rangeEnd = Math . Min ( end , entityLength - 1 ) ;
151+ isSatisfiable = true ;
152+ return true ;
153+ }
154+
155+ if ( startPart . Length > 0 && endPart . Length == 0 ) {
156+ if ( ! long . TryParse ( startPart , NumberStyles . None , CultureInfo . InvariantCulture , out var start ) )
157+ return false ;
158+
159+ if ( start < 0 )
160+ return false ;
161+
162+ if ( start >= entityLength ) {
163+ isSatisfiable = false ;
164+ return true ;
165+ }
166+
167+ rangeStart = start ;
168+ rangeEnd = entityLength - 1 ;
169+ isSatisfiable = true ;
170+ return true ;
171+ }
172+
173+ if ( startPart . Length == 0 && endPart . Length > 0 ) {
174+ if ( ! long . TryParse ( endPart , NumberStyles . None , CultureInfo . InvariantCulture , out var suffixLength ) )
175+ return false ;
176+
177+ if ( suffixLength <= 0 )
178+ return false ;
179+
180+ if ( entityLength == 0 ) {
181+ isSatisfiable = false ;
182+ return true ;
183+ }
184+
185+ if ( suffixLength >= entityLength ) {
186+ rangeStart = 0 ;
187+ }
188+ else {
189+ rangeStart = entityLength - suffixLength ;
190+ }
191+
192+ rangeEnd = entityLength - 1 ;
193+ isSatisfiable = true ;
194+ return true ;
195+ }
196+
197+ return false ;
198+ }
199+ }
0 commit comments