22
33import com .google .common .base .Throwables ;
44import java .io .StringReader ;
5+ import java .net .URI ;
56import java .nio .charset .StandardCharsets ;
67import java .time .Duration ;
78import java .time .Instant ;
2021import org .springframework .http .HttpHeaders ;
2122import org .springframework .http .client .reactive .ReactorClientHttpConnector ;
2223import org .springframework .lang .NonNull ;
24+ import org .springframework .lang .Nullable ;
2325import org .springframework .util .CollectionUtils ;
2426import org .springframework .web .reactive .function .client .WebClient ;
27+ import org .springframework .web .reactive .function .client .WebClientException ;
2528import org .springframework .web .util .UriComponentsBuilder ;
2629import org .springframework .web .util .UriUtils ;
30+ import reactor .core .publisher .Flux ;
31+ import reactor .core .publisher .Mono ;
2732import reactor .netty .http .client .HttpClient ;
2833import run .halo .feed .telemetry .TelemetryEndpoint ;
2934
@@ -82,7 +87,7 @@ RssXmlBuilder withExternalUrl(String externalUrl) {
8287 return this ;
8388 }
8489
85- public String toXmlString () {
90+ public Mono < String > toXmlString () {
8691 Document document = DocumentHelper .createDocument ();
8792
8893 Element root = DocumentHelper .createElement ("rss" );
@@ -136,9 +141,10 @@ public String toXmlString() {
136141 }
137142
138143 var items = rss2 .getItems ();
139- createItemElementsToChannel (channel , items );
140144
141- return document .asXML ();
145+ return createItemElementsToChannel (channel , items )
146+ .thenReturn (document )
147+ .map (Document ::asXML );
142148 }
143149
144150 private void copyAttributesAndChildren (Element target , Element source ) {
@@ -171,14 +177,16 @@ private Element parseXmlString(String xml) throws DocumentException {
171177 }
172178 }
173179
174- private void createItemElementsToChannel (Element channel , List <RSS2 .Item > items ) {
180+ private Mono < Void > createItemElementsToChannel (Element channel , List <RSS2 .Item > items ) {
175181 if (CollectionUtils .isEmpty (items )) {
176- return ;
182+ return Mono . empty () ;
177183 }
178- items .forEach (item -> createItemElementToChannel (channel , item ));
184+ return Flux .fromIterable (items )
185+ .flatMap (item -> createItemElementToChannel (channel , item ))
186+ .then ();
179187 }
180188
181- private void createItemElementToChannel (Element channel , RSS2 .Item item ) {
189+ private Mono < Void > createItemElementToChannel (Element channel , RSS2 .Item item ) {
182190 Element itemElement = channel .addElement ("item" );
183191 itemElement .addElement ("title" )
184192 .addCDATA (XmlCharUtils .removeInvalidXmlChar (item .getTitle ()));
@@ -205,18 +213,19 @@ private void createItemElementToChannel(Element channel, RSS2.Item item) {
205213 .addText (item .getAuthor ());
206214 }
207215
216+ Mono <Void > handleEnclosure = Mono .empty ();
208217 if (StringUtils .isNotBlank (item .getEnclosureUrl ())) {
209218 var enclosureElement = itemElement .addElement ("enclosure" )
210219 .addAttribute ("url" , item .getEnclosureUrl ())
211220 .addAttribute ("type" , item .getEnclosureType ());
212-
213221 var enclosureLength = item .getEnclosureLength ();
222+ enclosureElement .addAttribute ("length" , enclosureLength );
214223 if (StringUtils .isBlank (enclosureLength )) {
215224 // https://www.rssboard.org/rss-validator/docs/error/MissingAttribute.html
216- var fileBytes = getFileSizeBytes (item .getEnclosureUrl ());
217- enclosureLength = String .valueOf (fileBytes );
225+ handleEnclosure = getFileSizeBytes (item .getEnclosureUrl ())
226+ .doOnNext (fileBytes -> enclosureElement .addAttribute ("length" , String .valueOf (fileBytes )))
227+ .then ();
218228 }
219- enclosureElement .addAttribute ("length" , enclosureLength );
220229 }
221230
222231 nullSafeList (item .getCategories ())
@@ -264,6 +273,7 @@ private void createItemElementToChannel(Element channel, RSS2.Item item) {
264273 }
265274 }
266275 });
276+ return handleEnclosure ;
267277 }
268278
269279 private String getDescriptionWithTelemetry (RSS2 .Item item ) {
@@ -298,23 +308,47 @@ static String instantToString(Instant instant) {
298308 .format (DateTimeFormatter .RFC_1123_DATE_TIME );
299309 }
300310
311+
301312 @ NonNull
302- private Long getFileSizeBytes (String url ) {
313+ Mono < Long > getFileSizeBytes (String url ) {
303314 return webClient .get ()
304- .uri (url )
315+ .uri (URI . create ( url ) )
305316 .header (HttpHeaders .USER_AGENT , UA )
306317 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
307318 .header (HttpHeaders .RANGE , "bytes=0-0" )
319+ .headers (headers -> {
320+ if (StringUtils .isNotBlank (externalUrl )) {
321+ // For Referrer anti-hotlinking
322+ headers .set (HttpHeaders .REFERER , externalUrl );
323+ }
324+ })
308325 .retrieve ()
309326 .toBodilessEntity ()
310327 .map (HttpEntity ::getHeaders )
311- .mapNotNull (headers -> headers .getFirst (HttpHeaders .CONTENT_LENGTH ))
312- .map ( Long :: parseLong )
328+ .mapNotNull (headers -> headers .getFirst (HttpHeaders .CONTENT_RANGE ))
329+ .mapNotNull ( RssXmlBuilder :: parseLengthOfContentRange )
313330 .doOnError (e -> log .debug ("Failed to get file size from url: {}" , url ,
314331 Throwables .getRootCause (e ))
315332 )
316333 .onErrorReturn (0L )
317- .blockOptional ()
318- .orElse (0L );
334+ .defaultIfEmpty (0L );
319335 }
336+
337+ @ Nullable
338+ static Long parseLengthOfContentRange (@ Nullable String contentRange ) {
339+ if (contentRange == null ) {
340+ return null ;
341+ }
342+ // Refer to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range#syntax
343+ var range = contentRange .split ("/" );
344+ if (range .length != 2 ) {
345+ return null ;
346+ }
347+ var lengthStr = range [1 ];
348+ if ("*" .equals (lengthStr )) {
349+ return null ;
350+ }
351+ return Long .parseLong (range [1 ]);
352+ }
353+
320354}
0 commit comments