From e3523d224d33d90d8a645a243512cc520aaab07f Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 11 Feb 2026 09:19:55 +0200 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B5=D0=BC=20=D0=B8=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B4=D0=B0=D1=87=D0=B0=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 ++ src/main/java/im/rosetta/Main.java | 52 ++++++++++- src/main/java/im/rosetta/api/CdnResource.java | 85 ++++++++++++++++++ .../java/im/rosetta/storage/FileStore.java | 70 +++++++++++++++ target/classes/im/rosetta/Main.class | Bin 538 -> 3693 bytes .../classes/im/rosetta/api/CdnResource.class | Bin 0 -> 4621 bytes .../im/rosetta/storage/FileStore.class | Bin 0 -> 3918 bytes 7 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/main/java/im/rosetta/api/CdnResource.java create mode 100644 src/main/java/im/rosetta/storage/FileStore.java create mode 100644 target/classes/im/rosetta/api/CdnResource.class create mode 100644 target/classes/im/rosetta/storage/FileStore.class diff --git a/pom.xml b/pom.xml index d1a0f85..1d4f395 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,11 @@ jersey-media-json-jackson ${jersey.version} + + org.glassfish.jersey.media + jersey-media-multipart + ${jersey.version} + org.slf4j slf4j-simple diff --git a/src/main/java/im/rosetta/Main.java b/src/main/java/im/rosetta/Main.java index 70dda04..c1ce47b 100644 --- a/src/main/java/im/rosetta/Main.java +++ b/src/main/java/im/rosetta/Main.java @@ -1,7 +1,55 @@ package im.rosetta; +import im.rosetta.api.CdnResource; +import im.rosetta.storage.FileStore; +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.server.ResourceConfig; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; + public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); + public static void main(String[] args) throws IOException, InterruptedException { + int port = resolvePort(args); + Path filesDir = Path.of("files"); + + FileStore fileStore = new FileStore(filesDir); + + ResourceConfig config = new ResourceConfig() + .register(MultiPartFeature.class) + .register(new CdnResource(fileStore)); + + URI baseUri = URI.create("http://0.0.0.0:" + port + "/"); + HttpServer server = GrizzlyHttpServerFactory.createHttpServer(baseUri, config, false); + Runtime.getRuntime().addShutdownHook(new Thread(server::shutdownNow)); + server.start(); + + System.out.println("CDN started at " + baseUri); + Thread.currentThread().join(); + } + + private static int resolvePort(String[] args) { + // Если порт указан аргументом — используем его. + if (args != null && args.length > 0) { + try { + return Integer.parseInt(args[0]); + } catch (NumberFormatException ignored) { + } + } + + // Если порт задан в окружении — используем его. + String envPort = System.getenv("PORT"); + if (envPort != null && !envPort.isBlank()) { + try { + return Integer.parseInt(envPort); + } catch (NumberFormatException ignored) { + } + } + + // Значение по умолчанию. + return 8080; } } \ No newline at end of file diff --git a/src/main/java/im/rosetta/api/CdnResource.java b/src/main/java/im/rosetta/api/CdnResource.java new file mode 100644 index 0000000..875d255 --- /dev/null +++ b/src/main/java/im/rosetta/api/CdnResource.java @@ -0,0 +1,85 @@ +package im.rosetta.api; + +import im.rosetta.storage.FileStore; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; +import org.glassfish.jersey.media.multipart.FormDataParam; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@Path("/") +public class CdnResource { + private final FileStore fileStore; + + public CdnResource(FileStore fileStore) { + this.fileStore = fileStore; + } + + @POST + @Path("u") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + public Response upload(@FormDataParam("file") InputStream inputStream) { + if (inputStream == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "file is required")) + .build(); + } + try { + String tag = fileStore.save(inputStream); + return Response.ok(Map.of("t", tag)).build(); + } catch (IOException e) { + return Response.serverError() + .entity(Map.of("error", "upload failed")) + .build(); + } + } + + @GET + @Path("d/{tag}") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Response download(@PathParam("tag") String tag) { + if (!isValidTag(tag)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "invalid tag")) + .build(); + } + try { + Optional file = fileStore.getIfValid(tag); + if (file.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "not found")) + .build(); + } + StreamingOutput stream = output -> Files.copy(file.get(), output); + return Response.ok(stream) + .header("Content-Disposition", "attachment; filename=\"" + tag + "\"") + .build(); + } catch (IOException e) { + return Response.serverError() + .entity(Map.of("error", "download failed")) + .build(); + } + } + + private boolean isValidTag(String tag) { + try { + UUID.fromString(tag); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } +} diff --git a/src/main/java/im/rosetta/storage/FileStore.java b/src/main/java/im/rosetta/storage/FileStore.java new file mode 100644 index 0000000..bc9fc93 --- /dev/null +++ b/src/main/java/im/rosetta/storage/FileStore.java @@ -0,0 +1,70 @@ +package im.rosetta.storage; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +public class FileStore { + private static final Duration TTL = Duration.ofDays(7); + private final Path filesDir; + + public FileStore(Path filesDir) throws IOException { + this.filesDir = filesDir.toAbsolutePath(); + Files.createDirectories(this.filesDir); + cleanupExpired(); + } + + public String save(InputStream inputStream) throws IOException { + cleanupExpired(); + String tag = UUID.randomUUID().toString(); + Path target = filesDir.resolve(tag); + Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING); + Files.setLastModifiedTime(target, java.nio.file.attribute.FileTime.from(Instant.now())); + return tag; + } + + public Optional getIfValid(String tag) throws IOException { + cleanupExpired(); + Path target = filesDir.resolve(tag).normalize(); + if (!target.startsWith(filesDir)) { + return Optional.empty(); + } + if (!Files.exists(target)) { + return Optional.empty(); + } + if (isExpired(target)) { + Files.deleteIfExists(target); + return Optional.empty(); + } + return Optional.of(target); + } + + /** + * Очищает неиспользуемые файлы, которые старше TTL. Вызывается при каждом сохранении и загрузке, а также при инициализации. + * @throws IOException + */ + public void cleanupExpired() throws IOException { + if (!Files.exists(filesDir)) { + return; + } + try (DirectoryStream stream = Files.newDirectoryStream(filesDir)) { + for (Path file : stream) { + if (Files.isRegularFile(file) && isExpired(file)) { + Files.deleteIfExists(file); + } + } + } + } + + private boolean isExpired(Path file) throws IOException { + Instant lastModified = Files.getLastModifiedTime(file).toInstant(); + return lastModified.plus(TTL).isBefore(Instant.now()); + } +} diff --git a/target/classes/im/rosetta/Main.class b/target/classes/im/rosetta/Main.class index a07f8ce44ee2f5480501e3d87acaed8575dc834a..6b8187b613a5e6b5a41d3379bb7cb7b4973f5e1f 100644 GIT binary patch literal 3693 zcmcIn`Fk5z6+O?kJhnWo9VJfUBqVO?CbHvXl0aCj(6n*t+C^TloCK#p8B5R7*wT!c z898+dw5%WHD8cG8wDrz)id>CTx8MJbR-J6W@M!*nh69u2nn1(_3v{{CbBZf+#&H>AbJr6cI3f^Rw2G3Bk2SCp!t26u^lLc5m;=`~L8;}& zjOiD2Jb;q|EzY7qD!DtfX3>VL8qB0L3}jGi)l=5aiTQ|dTQ3ml3( zHQE@wpNYXd1&rayf)~igxtV>dv zStsi;Ze>WMnQ%ucj%ygx@R-1(O=-=$)}1@W)r&^K_sbE-kB4L99LWS)7&Eif)vAui zF;3Zs?rYNm%UddoyuOhmytVVdBS-{ zKN?XC*ApBKQ#dmX2|QOIr#V%~W_4sRN0rh@ph#Aj1{0x76RaLLblk*ySPI<=3)Z#d zj?n#lN+4C*idn44X;#8h^=P-#tah-GsuO5IStZj4cWh}?eZW*S6o-e3 z2IARn5Or{Ltl&0*loi4vAvMspWtCJn*@->;Ibnd#ZNIA!>C9p6w|XlIg2dqrvMn>t>`w}{{Ju2Kig1l~|?Y50!7 ziJG3mo5`5tmP~*9CcsTJ688Q?UG_vW;L&G%GrK%#mV?`ihUZvgGC_A#O_v9wZcOKt z7v{!Q$MHSiHOrIIFE}|bj-TLX8h$D;P{U!_E6%bsLJwhQ*siiE{9NFr`oxG2f6j$h-q8h*phv@6oISz5@MjKEyn>L#z^HQu~RwIP9j{{N%ilahwXkm!2G8m0p} z>x+03rhe6f_Bc$e=~!1Kq05VE^^|kFJBB}Sx&xaO&FHnM{-DUpT)*k}3wHhq+m>$7 zY}9@8p1?!9NNpwv4S#94`Zi#Ww8O*+)x$d}^Mp4Q@SAlW?flmGX7E?=H#W46j#v56 zf;hj`G;lw<_^pQv_P!+wG6Me0(<7gFcn%u;L z^TX?SXlfJ5`E?8>&aC0=CN9jMUB{c(@Ya#GqZ@b!P{lh(Vtui`w$>^}N7}2H*u?aF zU;F$T-hFce*Fj?W{NBFyHGJST$i7$=%NuZjHMmtgy@n6f;Exa_2r))@-2`|Ty*Pn` z7~sDEgU=Vxhs!vEG1il(aE#6GIDD?SgZ?1o@h}uTi|1%3#vbxfe2g@7qlk~=6Ab?_ zviKxEMK&+6OFxen&_;__@M(O8>&I{jpT*}$V_g03;7&!&FSt*g!SK-E(KB@K6?6@? zUaaEtchiZlRPnX+84i=3mtIEa88tk)@5GCDxu=tUd+=?X#3`KSZ)>a7D2%1eloXm6I|>UDRxpVZ0=dkXanxA326H?2U?GKt zg$r;mZh?5v*5*Ht|2vQG{!7(-tv^12OHQ86>r+UBmZqtwFOvHtc1bpir;oQ~y0q-k zR#fu4QqjKpwdZ*}Ddt%@EsHc+p4I3o>^OBUA3o%TyN(P!4mAgg);|N5K1T|Dv)SP? zh2Pa3Khpz$5q2#Ra!!7Oz&Grvs`zu;8w)R;5rMc6uBbOV@9e*AFpQ{?2wT+HW=Asn O2Xf3nD#cJVAo>9}XfIj- diff --git a/target/classes/im/rosetta/api/CdnResource.class b/target/classes/im/rosetta/api/CdnResource.class new file mode 100644 index 0000000000000000000000000000000000000000..38e4475c99e76f1821bfab381a6a8175c22de7e5 GIT binary patch literal 4621 zcmcIn347dD6+O>(EPEn1#%&fSrZ|a{dW~pFleFVF-W?a)<9G`s&>+vqW7!&MDruaA z(w41d-?z5x`xc-rt!o05vX&Np5+87$q*-kbAABG27mf6E?|b*2d+vEpZ~XK2D*%S@ zHw85Uhs})UI-cSCx~6AMZ6syS8J?4KlZJvif#x-RL)R?bPHTzfH6!T@G_IJIG4DIB zA#nFpiND7&J#A>?`S)>w`hL?k{Q-faoz+ddmIUfXoRkqmE%vHVpbFG>b}cnR;0_gg zP%p4=%CwD{TxQvD7xZO|0kljxN!?n~T~j_sgLVF@=?S!zu2_+r3@w+n96cp)x^pVX z(sZ;*JDc<8eb>-4@h*<8>n?%0?rE;4C7H2CaI%i=8T4W*zL{l6+KS_5`ck^D zZ+Dn=om4IvI5g|otYs#HptLp5u?3Em2R*C1ddBb#w=DY8Q;wU~(w6RdE2g)qtr@Oo z+|)8g%G9-D5aSGDlraR8FupoTj7A(ohl1l=stSA^^MoqrsW^d?0(*ytMz76{U0NKQ zUl8c1K%flw$T1#6C%P4M2^^`ijEWxgveI6#n!voQ^sY6{&t0|f9eD-}=S(YQxbX&@ zVg(J?b=(HLl^r8dXg9rf*LWmnx<+a*?!##XZxd(=XqNL$OPkiSD(=S_ff{E;;Qok$ zC5;wmsO7B}Fp@+02?)YDJgDFSfu3qDs+h^H8yQO*_r1Tx37_k|qzSc)Luv z##xt-(<)|=An82A-7wrSSvUc$dbm4O{SxaLDoFc^&XT7T%(F)eN|G46kuvpJY_m-A6nyh+R zm_bF0OpHn8Qc~KZ4DT`OO{u6jN#F4Myg)Fih!w80l($eEVifUZ6-kg8G1FVpEi<(s zv)#W_gjX8Co*-z{MKjgTbtPL?RhU?#f7nN*g=^!9#hK9tP^23v_3bN8&Q7&(FTJkf z2I#csx@-C@)Pqq>y6tn@Ik`I;fwDvtkp21r6(7WhCxQ3>^dpX zn&f7DAg04d6+BJjmxF=d;1vbGC9Stj93~oy*?1nr$MHLXr~m)3 zMJQh_$+tx2szC7T2K+(pg#2nsuFC$6w@Xp6d6qtZOdJzXCvDqsN8}0Gpf2yLID(O_ z;Q?5|YeDN(thEz86ui#wt!)67nR4EcOE}6R)N*fP8_G{7-tFPNf(Bk2`819i{sFP> zm-)ZvB{V(HryA_z^^U;vFzRrGwryzU*n!X*Z{mFNUo4oFJ%c-O7wz|R>>hd?;O{|> z9`ewN!}8`TgvW{63iofu-8ACY9S@12WqT0P>zqc0JlOLZ4!(e+TezpCuSMI!y_-0@ ziSy5Ozlf%uO$<%9O!RVe3zKqmdJ9vun=RVONzsQ>nB#mhq5GindYs{PFz^!$_#}hu zpj8)v>ZWB6H1yK05B+?)z+hl{q%p^ceeVfcg0NdDFZ=q&}sJwxi8 zB{j|wwFg+wv$XCfmgkAp0Fk@Er$Jr^cpb(BMtJBRB}!wQYlO3n6Osu$jEh*sB-VK^ z5k5^CG0y_RR)8y8g5O5%c#1JikYFFdM~TSQ0@fd+k4nHotB*(Mx1zS~A2_bS`WLxZ zqd+M5guKkrfG1BnNanBdSJEujvxQGxuDdl|dy6BoKNNg{ZzN$_0{9ago8{;wral*h z+a7ur!i`1YHgJV6;mc7V^$-)Bt2rh$kYmqq#(MtF;n^U9GrX_inA*LGuf2+IF_7;R zyN}*{Hyo@dla#6c2tVds9e%>68vHaU$X<>_oVtad@kI+?y~qGJ@hjZMt9<=?{E_2_ aId+a8K7Xo{C)!T@d9W3K!C&zPTK^4P`1tMs literal 0 HcmV?d00001 diff --git a/target/classes/im/rosetta/storage/FileStore.class b/target/classes/im/rosetta/storage/FileStore.class new file mode 100644 index 0000000000000000000000000000000000000000..9102e2dcba47724d5b0908433d7b0297b2b747ab GIT binary patch literal 3918 zcmaJ^`F9i775<(rdyp6m0mopoIV5&$Nk%DzEXW~oY=_E`4MheMk~SSnBYQy7D5DYB z>5}fDZQ8Wy3fz4!a>@~;2+ zzxO`?FoxS2+64Ak#e`#5OxHCM71wr*1vBxCRWLLBZfXb#Y&&mUG!g})w2(M4f8NZx z0`0T2X@TyvdgEF}Gci$h4A-(tNr6py9}*$KIsIBB?xQere)uu7JDjGBgq z&j^IZ?VQ;`ujG#w!B*U(VVl7AjRtgV#}0u`J3nD8R|IwrO%1p5BMgCib#$OpU~Aec znKRYmyy?su^97TN({|P=D?Z93;Hd ziRPOK+Obc^E<`q?9|ttt*8t+vr{jJ+AkgL7WAhceP<2hojzG^)9gj~sDe(;I(6C9v zkidX4+!Qff*RkfQL&a{av5FvyxQ1B6(WbnNa7D)m4hr0pbxgxG88F7yb}W;uo@&X! z^8s4!U^UqiK!gAu((w^Q2s&FZjZ(Edxm2dr923A@v}kr;`fl0C%n4PDiwxgz-91Ts zsa$n4E+G^fc2%l!%PK7-BX|^#Yj}({6=PM`DkM&yPEF`I3Mz~^Mk!|(y0)j4iQSM~Z%c-IOvlq07tkD&L6yD^)qPzr*^(d)lLEW^aUHkI%O_O6 zg>f7y4IdTg_lsm)A~2lXny8K`Jj=R0HF+{UHa_{{l<-?1y4^P$H|dvEXWR~I9#7q?24}8ESPN2 z5q_rfXN-cy;<`%)szmn^%qXxN!WnC!WVls_;Evrnaec=|19%mfV@X*J=XG3=O)gxr zog$ri*_69Q9VOUwl96}Z%A6(J-W_`Gxd>hYdo3>$?=tY2*5RTm&|wzKE)%ucI(XJq->-Z|Z zMwUwErA8}X_WGj0k(O|RSr|CR2)>4IYWRl0UVqC82k zTxJ`u$O~%M`eJAbC*@4gSy8u-!2UoX1ZZM;h2#`Odi>MJa=9^q9J#xOJ5-vag%O3x(jz43CXv>8vYw5ANan*iZ zmMVwLLSnZ5WvpmdOKv02eJxt*s_9t8`!cyIPn&s;q%yfY=X|om=LGb^x&}=uT8Odf zWbCSwHD%JUBIQF(x(8*3<384KJ4etq^vTJQ-?qz%i_cDeh9PTwGgnbc$e&0z^cuR} zLH7-=+R($#PRf82>`t`tu=N8iKVXlxbx1xXu-zKenOTX?XB z;koDv5^HLE=#X?LgdONYHyyke`)E*_>G4$I5Ds%irOIhRV-MrV2Gg(8#So`g0i!ynEESaWlW}| zyV6gRML$;?aMeu z_<4XzOJxgKxN5F-My}g3#)A5_=Uco!e=oP&8Hs5;5 hECU3L)3u3pK>C_t?o;lw!H|Z(KqjQXEp_^X{{t)X@wNZ} literal 0 HcmV?d00001