Repository: linuxserver/fleet Branch: master Commit: f331c22cd833 Files: 287 Total size: 2.1 MB Directory structure: gitextract_h6oaxvsz/ ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── config/ │ ├── example.fleet.properties │ ├── log4j2.local.xml │ └── log4j2.release.xml ├── gradle/ │ └── wrapper/ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src/ ├── main/ │ ├── java/ │ │ └── io/ │ │ └── linuxserver/ │ │ └── fleet/ │ │ ├── auth/ │ │ │ ├── AuthenticatedUser.java │ │ │ ├── AuthenticationDelegate.java │ │ │ ├── AuthenticationResult.java │ │ │ ├── DefaultAuthenticationDelegate.java │ │ │ ├── UserCredentials.java │ │ │ ├── authenticator/ │ │ │ │ ├── DefaultUserAuthenticator.java │ │ │ │ └── UserAuthenticator.java │ │ │ └── security/ │ │ │ ├── PBKDF2PasswordEncoder.java │ │ │ ├── PasswordEncoder.java │ │ │ └── util/ │ │ │ └── SaltGenerator.java │ │ ├── core/ │ │ │ ├── AbstractAppController.java │ │ │ ├── BaseRuntimeLoader.java │ │ │ ├── FleetAppController.java │ │ │ ├── FleetRuntime.java │ │ │ ├── Main.java │ │ │ ├── PropertiesLoader.java │ │ │ ├── ServiceProvider.java │ │ │ ├── config/ │ │ │ │ ├── AppProperties.java │ │ │ │ ├── DatabaseConnectionProperties.java │ │ │ │ ├── Version.java │ │ │ │ ├── VersionProperties.java │ │ │ │ └── WebConfiguration.java │ │ │ └── db/ │ │ │ ├── DatabaseConnection.java │ │ │ ├── DatabaseProvider.java │ │ │ └── DefaultDatabaseProvider.java │ │ ├── db/ │ │ │ ├── DefaultDatabaseConnection.java │ │ │ ├── PoolingDatabaseConnection.java │ │ │ ├── dao/ │ │ │ │ └── Utils.java │ │ │ ├── migration/ │ │ │ │ └── DatabaseVersion.java │ │ │ └── query/ │ │ │ ├── InsertUpdateResult.java │ │ │ ├── InsertUpdateStatus.java │ │ │ ├── LimitOffset.java │ │ │ └── LimitedResult.java │ │ ├── dockerhub/ │ │ │ ├── DockerHubException.java │ │ │ ├── model/ │ │ │ │ ├── DockerHubV2Image.java │ │ │ │ ├── DockerHubV2ImageListResult.java │ │ │ │ ├── DockerHubV2NamespaceLookupResult.java │ │ │ │ ├── DockerHubV2ScanResult.java │ │ │ │ ├── DockerHubV2Tag.java │ │ │ │ ├── DockerHubV2TagDigest.java │ │ │ │ └── DockerHubV2TagListResult.java │ │ │ └── util/ │ │ │ └── DockerTagFinder.java │ │ ├── exception/ │ │ │ └── SaveException.java │ │ └── v2/ │ │ ├── LoggerOwner.java │ │ ├── Utils.java │ │ ├── cache/ │ │ │ ├── AbstractItemCache.java │ │ │ ├── BasicItemCache.java │ │ │ ├── ImageCache.java │ │ │ ├── ItemCache.java │ │ │ ├── RepositoryCache.java │ │ │ └── ScheduleCache.java │ │ ├── client/ │ │ │ ├── docker/ │ │ │ │ ├── AbstractDockerApiClient.java │ │ │ │ ├── DockerApiClient.java │ │ │ │ ├── DockerImageNotFoundException.java │ │ │ │ ├── converter/ │ │ │ │ │ ├── AbstractDockerResponseConverter.java │ │ │ │ │ └── DockerResponseConverter.java │ │ │ │ ├── dockerhub/ │ │ │ │ │ ├── DockerHubApiClient.java │ │ │ │ │ ├── DockerHubAuthenticator.java │ │ │ │ │ ├── DockerHubCredentials.java │ │ │ │ │ ├── DockerHubImageConverter.java │ │ │ │ │ ├── DockerHubTagConverter.java │ │ │ │ │ ├── IDockerHubAuthenticator.java │ │ │ │ │ └── NoOpDockerHubAuthenticator.java │ │ │ │ ├── github/ │ │ │ │ │ ├── GitHubContainerRegistryClient.java │ │ │ │ │ ├── GitHubImageConverter.java │ │ │ │ │ ├── GitHubTagConverter.java │ │ │ │ │ └── model/ │ │ │ │ │ ├── GitHubImage.java │ │ │ │ │ └── GitHubTag.java │ │ │ │ └── queue/ │ │ │ │ ├── AsyncDockerApiRequest.java │ │ │ │ ├── AsyncDockerApiResponse.java │ │ │ │ ├── DockerApiDelegate.java │ │ │ │ ├── DockerApiTaskConsumer.java │ │ │ │ ├── DockerImageMissingUpdateResponse.java │ │ │ │ ├── DockerImageUpdateRequest.java │ │ │ │ ├── DockerImageUpdateResponse.java │ │ │ │ └── TaskQueue.java │ │ │ └── rest/ │ │ │ ├── HttpException.java │ │ │ ├── RestClient.java │ │ │ ├── RestResponse.java │ │ │ ├── marshalling/ │ │ │ │ ├── JacksonMarshallingStrategy.java │ │ │ │ └── MarshallingStrategy.java │ │ │ └── proxy/ │ │ │ ├── LazyLoadPayloadProxy.java │ │ │ └── PayloadProxy.java │ │ ├── db/ │ │ │ ├── AbstractDAO.java │ │ │ ├── DbUpdateStatus.java │ │ │ ├── DefaultImageDAO.java │ │ │ ├── DefaultScheduleDAO.java │ │ │ ├── DefaultUserDAO.java │ │ │ ├── ImageDAO.java │ │ │ ├── ImageTemplateFactory.java │ │ │ ├── ScheduleDAO.java │ │ │ ├── UserDAO.java │ │ │ └── Utils.java │ │ ├── file/ │ │ │ └── FileManager.java │ │ ├── key/ │ │ │ ├── AbstractDatabaseKey.java │ │ │ ├── AbstractHasKey.java │ │ │ ├── AbstractLookupKey.java │ │ │ ├── AlertKey.java │ │ │ ├── HasKey.java │ │ │ ├── ImageKey.java │ │ │ ├── ImageLookupKey.java │ │ │ ├── Key.java │ │ │ ├── RepositoryKey.java │ │ │ ├── ScheduleKey.java │ │ │ ├── TagBranchKey.java │ │ │ └── UserKey.java │ │ ├── service/ │ │ │ ├── AbstractAppService.java │ │ │ ├── ImageService.java │ │ │ ├── ScheduleService.java │ │ │ ├── SynchronisationService.java │ │ │ ├── UserService.java │ │ │ └── util/ │ │ │ └── TemplateMerger.java │ │ ├── thread/ │ │ │ ├── AbstractAppTask.java │ │ │ ├── AbstractAppThread.java │ │ │ ├── AbstractTaskQueueConsumer.java │ │ │ ├── AsyncTask.java │ │ │ ├── AsyncTaskDelegate.java │ │ │ ├── AsyncTaskResponse.java │ │ │ ├── TaskExecutionException.java │ │ │ ├── TaskResponseControllerProxy.java │ │ │ ├── ThreadStatus.java │ │ │ └── schedule/ │ │ │ ├── AbstractAppSchedule.java │ │ │ ├── AppSchedule.java │ │ │ ├── CheckAppVersionSchedule.java │ │ │ ├── ScheduleSpec.java │ │ │ ├── TidyHistoricDataSchedule.java │ │ │ ├── TimeWithUnit.java │ │ │ ├── cache/ │ │ │ │ └── RefreshCacheSchedule.java │ │ │ └── sync/ │ │ │ ├── AllImagesSyncSchedule.java │ │ │ ├── CleanRemovedImagesSchedule.java │ │ │ └── GetMissingImagesSchedule.java │ │ ├── types/ │ │ │ ├── AbstractSyncItem.java │ │ │ ├── AppAlert.java │ │ │ ├── FilePathDetails.java │ │ │ ├── HasSyncSpec.java │ │ │ ├── Image.java │ │ │ ├── ImageCountData.java │ │ │ ├── Repository.java │ │ │ ├── Tag.java │ │ │ ├── TagBranch.java │ │ │ ├── TagDigest.java │ │ │ ├── User.java │ │ │ ├── api/ │ │ │ │ ├── AbstractApiWrapper.java │ │ │ │ ├── ApiImagePullHistoryWrapper.java │ │ │ │ ├── ApiImageWrapper.java │ │ │ │ ├── ApiRepositoryWrapper.java │ │ │ │ ├── ApiScheduleWrapper.java │ │ │ │ └── external/ │ │ │ │ ├── AllImagesExternalApiResponse.java │ │ │ │ ├── ExternalApiImage.java │ │ │ │ ├── ExternalApiResponse.java │ │ │ │ └── templates/ │ │ │ │ ├── ApiDeviceTemplate.java │ │ │ │ ├── ApiEnvTemplate.java │ │ │ │ ├── ApiPortTemplate.java │ │ │ │ ├── ApiTemplateHolder.java │ │ │ │ └── ApiVolumeTemplate.java │ │ │ ├── docker/ │ │ │ │ ├── DockerCapability.java │ │ │ │ ├── DockerImage.java │ │ │ │ ├── DockerTag.java │ │ │ │ └── DockerTagManifestDigest.java │ │ │ ├── internal/ │ │ │ │ ├── AbstractParamRequest.java │ │ │ │ ├── ImageAppLogo.java │ │ │ │ ├── ImageGeneralInfoUpdateRequest.java │ │ │ │ ├── ImageOutlineRequest.java │ │ │ │ ├── ImageTemplateRequest.java │ │ │ │ ├── ImageUrlsUpdateRequest.java │ │ │ │ ├── RepositoryOutlineRequest.java │ │ │ │ ├── TagBranchOutlineRequest.java │ │ │ │ └── UserOutlineRequest.java │ │ │ └── meta/ │ │ │ ├── ExternalUrl.java │ │ │ ├── ExternalUrlKey.java │ │ │ ├── ImageCoreMeta.java │ │ │ ├── ImageMetaData.java │ │ │ ├── ItemSyncSpec.java │ │ │ ├── history/ │ │ │ │ ├── ImagePullHistory.java │ │ │ │ └── ImagePullStatistic.java │ │ │ └── template/ │ │ │ ├── AbstractTemplateItem.java │ │ │ ├── DeviceTemplateItem.java │ │ │ ├── EnvironmentTemplateItem.java │ │ │ ├── ImageTemplateHolder.java │ │ │ ├── PortTemplateItem.java │ │ │ ├── TemplateItem.java │ │ │ └── VolumeTemplateItem.java │ │ └── web/ │ │ ├── ApiException.java │ │ ├── AppRole.java │ │ ├── LocationUtils.java │ │ ├── Locations.java │ │ ├── PageModelAttributes.java │ │ ├── PageModelSpec.java │ │ ├── SessionAttributes.java │ │ ├── WebRouteController.java │ │ ├── freemarker/ │ │ │ ├── CustomFreemarkerTemplate.java │ │ │ └── Java8DateTimeMethod.java │ │ ├── request/ │ │ │ └── json/ │ │ │ ├── NewRepositoryRequest.java │ │ │ ├── UpdateImageSpecRequest.java │ │ │ └── UpdateRepositoryRequest.java │ │ └── routes/ │ │ ├── AbstractPageHandler.java │ │ ├── AdminImageController.java │ │ ├── AdminImageEditController.java │ │ ├── AdminRepositoryController.java │ │ ├── AdminScheduleController.java │ │ ├── AdminUserController.java │ │ ├── DefaultAccessManager.java │ │ ├── HomeController.java │ │ ├── ImageController.java │ │ ├── InternalApiController.java │ │ ├── LegacyExternalApiController.java │ │ └── LoginController.java │ └── resources/ │ ├── db/ │ │ └── migration/ │ │ ├── V1.0__CreateTables.sql │ │ ├── V1.10__UpdateImageViewWithRepository.sql │ │ ├── V1.1__CreateSprocs.sql │ │ ├── V1.2__CreateUserTable.sql │ │ ├── V1.3__CreateUserSprocs.sql │ │ ├── V1.4__AddDeprecationFields.sql │ │ ├── V1.5__UpdateImageSprocs.sql │ │ ├── V1.6__ExtendVersionColumn.sql │ │ ├── V1.7__RemoveCoalesce.sql │ │ ├── V1.8__PullHistoryAndImageMeta.sql │ │ ├── V1.9__ExtraTagFields.sql │ │ ├── V2.0__CreateV2TablesAndSprocs.sql │ │ ├── V2.1__MigrateToNewTables.sql │ │ ├── V2.2__MetaDataTables.sql │ │ ├── V2.3__UpdateImageViewForCoreMeta.sql │ │ ├── V2.4__UpdateUserSprocs.sql │ │ ├── V2.5__BranchRemovalSproc.sql │ │ ├── V2.6__AddCleanSchedule.sql │ │ ├── V2.7__UpdateImageTemplateSprocs.sql │ │ └── V2.8__FixStoreCoreMetaOutputBug.sql │ ├── static/ │ │ └── assets/ │ │ ├── css/ │ │ │ ├── app.css │ │ │ ├── bootstrap.css │ │ │ └── prism.css │ │ └── js/ │ │ ├── admin.js │ │ ├── app.js │ │ ├── fontawesome-all.js │ │ ├── jquery.tablesorter.js │ │ └── prism.js │ ├── version.properties │ └── views/ │ ├── pages/ │ │ ├── admin/ │ │ │ ├── image-edit.ftl │ │ │ ├── images.ftl │ │ │ ├── repositories.ftl │ │ │ ├── schedules.ftl │ │ │ ├── template-components/ │ │ │ │ ├── image-template-devices.ftl │ │ │ │ ├── image-template-environment.ftl │ │ │ │ ├── image-template-misc.ftl │ │ │ │ ├── image-template-ports.ftl │ │ │ │ └── image-template-volumes.ftl │ │ │ └── users.ftl │ │ ├── error.ftl │ │ ├── home.ftl │ │ ├── image.ftl │ │ └── login.ftl │ ├── prebuilt/ │ │ ├── base.ftl │ │ ├── docker-example.ftl │ │ ├── fleet-title.ftl │ │ ├── image-list-item.ftl │ │ ├── image-list-table-item.ftl │ │ └── system-alert.ftl │ └── ui/ │ ├── components/ │ │ ├── dropdown.ftl │ │ ├── message.ftl │ │ ├── modal.ftl │ │ ├── navbar.ftl │ │ └── pagination.ftl │ ├── elements/ │ │ ├── box.ftl │ │ ├── button.ftl │ │ ├── display-field.ftl │ │ ├── media.ftl │ │ ├── notification.ftl │ │ ├── table.ftl │ │ └── tag.ftl │ ├── form/ │ │ └── input.ftl │ └── layout/ │ ├── container.ftl │ ├── footer.ftl │ ├── hero.ftl │ └── section.ftl └── test/ └── java/ └── io/ └── linuxserver/ └── fleet/ ├── auth/ │ └── security/ │ └── PBKDF2PasswordEncoderTest.java ├── dockerhub/ │ └── util/ │ └── DockerTagFinderTest.java └── v2/ └── thread/ └── schedule/ └── TimeWithUnitTest.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* .idea/ bin/ build/ out/ *.iml config/fleet.properties /config/fleet_static/* src/main/resources/assets/js/all*.js src/main/resources/assets/css/all*.css src/main/resources/log4j2.xml .gradle/ .classpath .project .settings/ .vscode/ **/.DS_Store ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # Fleet Fleet is a Docker Hub repository and image management tool for organisations (or individuals) who wish to display a list of all currently available images, along with their latest version and build status. The idea for this application was borne out of a necessity for the LinuxServer team to be able to provide a mechanism for its users to see the current build version of the images they use. Image information is retrieved via the Docker Hub API (v2) through a scheduled task, which runs at a given (configurable) interval. This task will synchronise all repositories owned by the user whose credentials are used to authorise the initial requests to Docker Hub. Fleet will store in memory a valid authorisation token and will reuse it until it expires, after which a new token will be requested. ## Management For administrators of the application, Fleet provides a way to manage which repositories get synchronised, and how each image's latest versioned tag should be masked. ### Repositories All repositories for the user are automatically retrieved upon start up, and by default not synchronised. The administration page allows you to toggle which repositories are synchronised (and thus displayed on the main page). #### Version Mask Depending on how an image gets built, its versioned tag may contain extraneous information, or wrap the inner application's version with build information. If you wish to represent the image's "version" as the wrapped application's version instead, you can apply a version mask using standard REGEX, which will pull out the specific part of the tag you wish to used for the version. Multiple capture groups are supported, and will be concatenated in match order. ### Images Each image in a repository is by default shown on the main page, along with their current version and build status (which is a manually set flag). When logged in, users are able to manually configure the visiblity and status of each image: #### Version Mask A specific version mask can be applied to an image, which will override the default repository mask (which applies to all images). Useful if a certain image uses different tagging standards. ## Documentation Full documentation can be found here: https://docs.linuxserver.io/general/fleet # Thanks A huge thank you to JetBrains who kindly provided us with a license for IntelliJ Ultimate. It is without question the best IDE for Java application development. ================================================ FILE: build.gradle ================================================ plugins { id 'java' } sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 repositories { mavenCentral() } version = '2.3.3' dependencies { // Logging implementation 'org.apache.logging.log4j:log4j-api:2.19.0' implementation 'org.apache.logging.log4j:log4j-core:2.19.0' implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0' // HTTP Framework implementation 'io.javalin:javalin:3.6.0' implementation 'org.freemarker:freemarker:2.3.31' implementation 'org.apache.httpcomponents:httpclient:4.5.13' // JSON Mapping/Marshalling implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6' implementation 'com.fasterxml.jackson.core:jackson-core:2.9.6' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.9.6' // Database runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:3.0.6' implementation 'org.flywaydb:flyway-core:8.2.0' implementation 'com.zaxxer:HikariCP:5.0.1' // MISC implementation 'org.apache.commons:commons-lang3:3.12.0' // Unit Testing testImplementation 'junit:junit:4.11' testImplementation 'org.mockito:mockito-core:4.8.0' } jar { manifest { attributes "Main-Class": "io.linuxserver.fleet.core.Main" } from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } duplicatesStrategy = DuplicatesStrategy.INCLUDE exclude 'META-INF/INDEX.LIST', 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' } task configureLogConfiguration(type: Copy) { def logFile = project.hasProperty('env') ? "${env}" : 'local' from "config/log4j2.${logFile}.xml" into 'src/main/resources' rename "log4j2.${logFile}.xml", 'log4j2.xml' } task buildVersionProperties() { doFirst { def versionProperties = new Properties() def propFile = new File("src/main/resources/version.properties") versionProperties.load(propFile.newDataInputStream()); versionProperties.setProperty("app.version", project.version.toString()) versionProperties.setProperty("app.build.user", System.getProperty("user.name")) versionProperties.setProperty("app.build.date", new Date().format("yyyy-MM-dd'T'HH:mm:ss")) versionProperties.setProperty("app.build.os", System.getProperty("os.name")) versionProperties.store(propFile.newWriter(), null) } } processResources.dependsOn configureLogConfiguration, buildVersionProperties ================================================ FILE: config/example.fleet.properties ================================================ # This is an example properties file for Fleet. # You should fill in your own values here and rename the file to "fleet.properties". # Runtime fleet.app.port=8080 # Database Connectivity fleet.database.driver=org.mariadb.jdbc.Driver fleet.database.url=jdbc:mariadb://:3306/fleet fleet.database.username= fleet.database.password= # DockerHub auth fleet.dockerhub.auth.enabled=true fleet.dockerhub.username=YOUR_USERNAME fleet.dockerhub.password=YOUR_PASSWORD_OR_AUTH_TOKEN ================================================ FILE: config/log4j2.local.xml ================================================ ================================================ FILE: config/log4j2.release.xml ================================================ ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ rootProject.name = 'fleet' ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/AuthenticatedUser.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth; import io.linuxserver.fleet.v2.types.User; import io.linuxserver.fleet.v2.web.AppRole; import java.util.Collections; import java.util.Set; public class AuthenticatedUser { private final User user; public AuthenticatedUser(final User user) { this.user = user; } public final String getName() { return user.getUsername(); } public final Set getRoles() { return Collections.singleton(user.getRole()); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/AuthenticationDelegate.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth; public interface AuthenticationDelegate { AuthenticationResult authenticate(String username, String password); String encodePassword(String rawPassword); } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/AuthenticationResult.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth; public class AuthenticationResult { private final boolean authenticated; private final AuthenticatedUser user; public AuthenticationResult(boolean authenticated, AuthenticatedUser user) { this.authenticated = authenticated; this.user = user; } public static AuthenticationResult notAuthenticated() { return new AuthenticationResult(false, null); } public boolean isAuthenticated() { return authenticated; } public AuthenticatedUser getUser() { return user; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/DefaultAuthenticationDelegate.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth; import io.linuxserver.fleet.auth.authenticator.UserAuthenticator; import io.linuxserver.fleet.auth.security.PasswordEncoder; public class DefaultAuthenticationDelegate implements AuthenticationDelegate { private final UserAuthenticator authenticator; public DefaultAuthenticationDelegate(final UserAuthenticator authenticator) { this.authenticator = authenticator; } @Override public AuthenticationResult authenticate(final String username, final String password) { return authenticator.authenticate(new UserCredentials(username, password)); } @Override public String encodePassword(final String rawPassword) { return authenticator.getPasswordEncoder().encode(rawPassword); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/UserCredentials.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth; public class UserCredentials { private final String username; private final String password; public UserCredentials(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public String getPassword() { return password; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/authenticator/DefaultUserAuthenticator.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth.authenticator; import io.linuxserver.fleet.auth.AuthenticatedUser; import io.linuxserver.fleet.auth.AuthenticationResult; import io.linuxserver.fleet.auth.UserCredentials; import io.linuxserver.fleet.auth.security.PasswordEncoder; import io.linuxserver.fleet.v2.service.UserService; import io.linuxserver.fleet.v2.types.User; public class DefaultUserAuthenticator implements UserAuthenticator { private final UserService userService; private final PasswordEncoder passwordEncoder; public DefaultUserAuthenticator(final UserService userService, final PasswordEncoder passwordEncoder) { this.userService = userService; this.passwordEncoder = passwordEncoder; } @Override public AuthenticationResult authenticate(final UserCredentials userCredentials) { final User user = userService.lookUpUser(userCredentials.getUsername()); if (null != user && getPasswordEncoder().matches(userCredentials.getPassword(), user.getPassword())) { return new AuthenticationResult(true, new AuthenticatedUser(user)); } return AuthenticationResult.notAuthenticated(); } @Override public PasswordEncoder getPasswordEncoder() { return passwordEncoder; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/authenticator/UserAuthenticator.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth.authenticator; import io.linuxserver.fleet.auth.AuthenticationResult; import io.linuxserver.fleet.auth.UserCredentials; import io.linuxserver.fleet.auth.security.PasswordEncoder; /** *

* Provides a mechanism for the application to authenticate a login request * from a user. *

*/ public interface UserAuthenticator { /** *

* Performs an authentication check against the provided credentials and the repository * of currently stored users. *

*/ AuthenticationResult authenticate(UserCredentials userCredentials); PasswordEncoder getPasswordEncoder(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/security/PBKDF2PasswordEncoder.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth.security; import io.linuxserver.fleet.auth.security.util.SaltGenerator; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.Base64; /** *

* Uses the PBKDF2 crypto algorithm to encode and verify hashed passwords. *

*/ public class PBKDF2PasswordEncoder implements PasswordEncoder { private static final int DEFAULT_HASH_WIDTH = 512; private static final int DEFAULT_ITERATIONS = 150051; private static final String PBKDF2 = "PBKDF2WithHmacSHA512"; private final SaltGenerator saltGenerator = new SaltGenerator(); private final byte[] secret; private final int hashWidth; private final int iterations; public PBKDF2PasswordEncoder(String secret) { this(secret, DEFAULT_HASH_WIDTH, DEFAULT_ITERATIONS); } public PBKDF2PasswordEncoder(String secret, int hashWidth, int iterations) { this.secret = secret.getBytes(StandardCharsets.UTF_8); this.hashWidth = hashWidth; this.iterations = iterations; } @Override public String encode(String rawPassword) { if (null == rawPassword) { throw new IllegalArgumentException("Password must not be null"); } return toBase64(encode(rawPassword, saltGenerator.generateSalt())); } @Override public boolean matches(String rawPassword, String encodedPassword) { byte[] decodedHash = fromBase64(encodedPassword); byte[] saltInHash = extractSalt(decodedHash); byte[] hashToVerify = encode(rawPassword, saltInHash); return passwordsMatch(decodedHash, hashToVerify); } /** *

* Compares the two byte arrays by performing a bitwise equality check against each individual * element of both arrays. *

* * @implNote I looked at a couple of implementations for doing this, and I preferred how Spring had implemented it. */ private boolean passwordsMatch(byte[] originalPassword, byte[] providedPassword) { if (originalPassword.length != providedPassword.length) { return false; } int result = 0; for (int i = 0; i < originalPassword.length; i++) { result |= originalPassword[i] ^ providedPassword[i]; } return result == 0; } /** *

* Performs the cryptographic hash against the raw password and the randomly generated salt value. This * also concatenates the provided secret into the salt. *

*/ private byte[] encode(String rawPassword, byte[] salt) { try { PBEKeySpec spec = new PBEKeySpec( rawPassword.toCharArray(), joinArrays(salt, secret), iterations, hashWidth ); return joinArrays(salt, SecretKeyFactory.getInstance(PBKDF2).generateSecret(spec).getEncoded()); } catch (GeneralSecurityException e) { throw new IllegalStateException("Unable to create password hash", e); } } /** *

* Converts a byte array into a base-64 encoded string. *

*/ private String toBase64(byte[] bytes) { return Base64.getEncoder().encodeToString(bytes); } /** *

* Converts a base64-encoded string into its raw byte value *

*/ private byte[] fromBase64(String input) { return Base64.getDecoder().decode(input); } /** *

* Obtains the specific bytes which represent the salt used in a previous hashed password. This is to * enable the comparision between the existing and new password. *

*/ private byte[] extractSalt(byte[] decodedHash) { return extractFromArray(decodedHash, 0, saltGenerator.getKeyLength()); } /** *

* Combines two byte arrays together in order. *

*/ private byte[] joinArrays(byte[] first, byte[] second) { byte[] result = Arrays.copyOf(first, first.length + second.length); System.arraycopy(second, 0, result, first.length, second.length); return result; } /** *

* Extracts a sub-array from the provided array. *

*/ private byte[] extractFromArray(byte[] array, int begin, int end) { int length = end - begin; byte[] subarray = new byte[length]; System.arraycopy(array, begin, subarray, 0, length); return subarray; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/security/PasswordEncoder.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth.security; /** *

* Provides a mechanism for a password to be encoded using a strong cryptographic algorithm. * The general idea of this interface has been taken from Spring's own implementation of this. * PasswordEncoder *

*/ public interface PasswordEncoder { /** *

* Encodes the raw password into a one-way encrypted hash. The result of which should be stored. *

* * @param rawPassword * The raw unencrypted password. * * @return * The hashed result. */ String encode(String rawPassword); /** *

* Determines if the provided raw password, when encoded, matches the stored encoded password. *

* * @param rawPassword * The raw password to check * @param encodedPassword * The originally encoded and stored password * @return * true if the passwords match, false if not. */ boolean matches(String rawPassword, String encodedPassword); } ================================================ FILE: src/main/java/io/linuxserver/fleet/auth/security/util/SaltGenerator.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth.security.util; import java.security.SecureRandom; public class SaltGenerator { private static final int KEY_LENGTH = 16; public byte[] generateSalt() { SecureRandom sr = new SecureRandom(); byte[] salt = new byte[KEY_LENGTH]; sr.nextBytes(salt); return salt; } public int getKeyLength() { return KEY_LENGTH; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/AbstractAppController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core; import io.linuxserver.fleet.core.config.AppProperties; import io.linuxserver.fleet.core.db.DatabaseProvider; import io.linuxserver.fleet.core.db.DefaultDatabaseProvider; import io.linuxserver.fleet.db.DefaultDatabaseConnection; import io.linuxserver.fleet.v2.cache.BasicItemCache; import io.linuxserver.fleet.v2.key.AlertKey; import io.linuxserver.fleet.v2.types.AppAlert; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public abstract class AbstractAppController { private final AppProperties appProperties; private final DatabaseProvider databaseProvider; private final BasicItemCache alertCache; public AbstractAppController() { this.appProperties = new PropertiesLoader().getProperties(); this.databaseProvider = new DefaultDatabaseProvider(new DefaultDatabaseConnection(appProperties.getDatabaseProperties())); this.alertCache = new BasicItemCache<>(); } public final DatabaseProvider getDatabaseProvider() { return databaseProvider; } public final AppProperties getAppProperties() { return appProperties; } public final List getAlerts() { return new ArrayList<>(alertCache.getAllItems()); } public final List getSystemAlerts() { return getAlerts().stream().filter(AppAlert::isSystemAlert).collect(Collectors.toList()); } public final void addAlert(final AppAlert appAlert) { alertCache.addItem(appAlert); } public final void clearAlert(final AlertKey alertKey) { alertCache.removeItem(alertKey); } protected void run() { } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/BaseRuntimeLoader.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** *

* Prints out all runtime arguments passed in as JVM arguments (-D). *

* * @author Josh Stark */ abstract class BaseRuntimeLoader { private static final Logger LOGGER = LoggerFactory.getLogger(BaseRuntimeLoader.class); BaseRuntimeLoader() { LOGGER.info("Initalising..."); LOGGER.info("Config base : " + FleetRuntime.CONFIG_BASE); LOGGER.info("Show Passwords : " + FleetRuntime.SHOW_PASSWORDS); LOGGER.info("Nuke database : " + FleetRuntime.NUKE_DATABASE); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/FleetAppController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core; import io.linuxserver.fleet.auth.AuthenticationResult; import io.linuxserver.fleet.core.config.WebConfiguration; import io.linuxserver.fleet.v2.client.docker.DockerApiClient; import io.linuxserver.fleet.v2.client.docker.dockerhub.DockerHubApiClient; import io.linuxserver.fleet.v2.client.docker.dockerhub.DockerHubAuthenticator; import io.linuxserver.fleet.v2.client.docker.dockerhub.IDockerHubAuthenticator; import io.linuxserver.fleet.v2.client.docker.dockerhub.NoOpDockerHubAuthenticator; import io.linuxserver.fleet.v2.client.docker.queue.DockerApiDelegate; import io.linuxserver.fleet.v2.client.rest.RestClient; import io.linuxserver.fleet.v2.db.DefaultImageDAO; import io.linuxserver.fleet.v2.db.DefaultScheduleDAO; import io.linuxserver.fleet.v2.db.DefaultUserDAO; import io.linuxserver.fleet.v2.file.FileManager; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.service.ImageService; import io.linuxserver.fleet.v2.service.ScheduleService; import io.linuxserver.fleet.v2.service.SynchronisationService; import io.linuxserver.fleet.v2.service.UserService; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.Repository; import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest; import io.linuxserver.fleet.v2.web.WebRouteController; /** *

* Primary entry point for the application. All contexts and resources are loaded * through this class. *

*/ public class FleetAppController extends AbstractAppController implements ServiceProvider { private final DockerApiDelegate dockerApiDelegate; private final ImageService imageService; private final ScheduleService scheduleService; private final SynchronisationService syncService; private final UserService userService; private final FileManager fileManager; public FleetAppController() { fileManager = new FileManager(this); imageService = new ImageService(this, new DefaultImageDAO(getDatabaseProvider())); scheduleService = new ScheduleService(this, new DefaultScheduleDAO(getDatabaseProvider())); dockerApiDelegate = new DockerApiDelegate(this, configureDockerApiClient()); syncService = new SynchronisationService(this); userService = new UserService(this, new DefaultUserDAO(getDatabaseProvider())); } private static FleetAppController instance; public static FleetAppController instance() { if (null == instance) { synchronized (FleetAppController.class) { if (null == instance) { instance = new FleetAppController(); } } } return instance; } @Override protected final void run() { super.run(); configureWeb(); scheduleService.initialiseSchedules(); } public final WebConfiguration getWebConfiguration() { return new WebConfiguration(getAppProperties()); } private void configureWeb() { new WebRouteController(this); } public final void handleException(final Exception e) { } public final boolean synchroniseImage(final ImageKey imageKey) { return syncService.synchroniseImage(imageKey); } public final void synchroniseRepository(final Repository repository) { syncService.synchroniseCachedRepository(repository); } public final DockerApiDelegate getConfiguredDockerDelegate() { return dockerApiDelegate; } public final ImageService getImageService() { return imageService; } public final Image storeUpdatedImage(final Image updatedImage) { return imageService.storeImage(updatedImage); } @Override public ScheduleService getScheduleService() { return scheduleService; } public final Repository verifyRepositoryAndCreateOutline(final RepositoryOutlineRequest request) { if (getConfiguredDockerDelegate().isRepositoryValid(request.getRepositoryName())) { final Repository repositoryOutline = getImageService() .createRepositoryOutline(new RepositoryOutlineRequest(request.getRepositoryName())); getSynchronisationService().synchroniseUpstreamRepository(repositoryOutline); return repositoryOutline; } throw new IllegalArgumentException("Repository " + request.getRepositoryName() + " does not exist upstream"); } @Override public final SynchronisationService getSynchronisationService() { return syncService; } @Override public final UserService getUserService() { return userService; } @Override public FileManager getFileManager() { return fileManager; } public final AuthenticationResult authenticateCredentials(final String username, final String password) { return userService.authenticateCredentials(username, password); } public final void trackBranch(final ImageKey imageKey, final String branchName) { getImageService().trackBranchOnImage(imageKey, branchName); synchroniseImage(imageKey); } private DockerApiClient configureDockerApiClient() { final RestClient dockerHubApiRestClient = new RestClient(); final IDockerHubAuthenticator dockerHubAuthenticator; if (getAppProperties().isDockerHubAuthEnabled()) { dockerHubAuthenticator = new DockerHubAuthenticator(getAppProperties().getDockerHubCredentials(), dockerHubApiRestClient); } else { dockerHubAuthenticator = new NoOpDockerHubAuthenticator(); } return new DockerHubApiClient(dockerHubApiRestClient, dockerHubAuthenticator); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/FleetRuntime.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core; public interface FleetRuntime { /** * If set will switch specific properties to allow more streamlined development */ boolean DEV_MODE = System.getProperty("enable.dev") != null; /** * Base directory for the config file. */ String CONFIG_BASE = System.getProperty("fleet.config.base"); /** * Whether or not logs should show passwords */ boolean SHOW_PASSWORDS = System.getProperty("fleet.show.passwords") != null; /** * Tells Fleet to completely wipe the database and recreate it. */ boolean NUKE_DATABASE = System.getProperty("fleet.nuke.database") != null; } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/Main.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core; public class Main { public static void main(String[] args) { FleetAppController.instance().run(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/PropertiesLoader.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core; import io.linuxserver.fleet.core.config.AppProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Objects; import java.util.Properties; /** *

* Loads in the application properties from disk. Properties must be provided as JVM arguments * as these will be used to create a connection to the underlying database, and any other * required connections. *

* * @author Josh Stark */ class PropertiesLoader extends BaseRuntimeLoader { private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesLoader.class); private final AppProperties properties; PropertiesLoader() { super(); try { createConfigFileIfNotProvided(); Properties properties = new Properties(); properties.load(new FileInputStream(FleetRuntime.CONFIG_BASE + "/fleet.properties")); properties.load(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream("version.properties"))); properties.setProperty("fleet.static.dirname", "fleet_static"); this.properties = new AppProperties(properties); printProperties(); if (!createStaticFileDirectory() || !createLogsDirectory()) { throw new RuntimeException("Unable to create config sub directories for Fleet. Check permissions"); } } catch (IOException e) { LOGGER.error("Unable to load config! Check JVM args and config directory", e); throw new RuntimeException(e); } } private void createConfigFileIfNotProvided() { try { File configFile = new File(FleetRuntime.CONFIG_BASE + "/fleet.properties"); if (!configFile.exists()) { if (!configFile.createNewFile()) { throw new RuntimeException("Unable to create base config for fleet."); } } } catch (IOException e) { throw new RuntimeException("Unable to create base config for fleet.", e); } } private boolean createStaticFileDirectory() { File staticFilesDir = new File(properties.getStaticFilesPath().toString()); if (staticFilesDir.exists()) { return true; } return staticFilesDir.mkdir(); } private boolean createLogsDirectory() { File logsDirectory = new File(FleetRuntime.CONFIG_BASE + "/logs"); if (logsDirectory.exists()) { return true; } return logsDirectory.mkdir(); } /** *

* The loaded properties, with accessible fields. *

* * @return * All application properties. */ AppProperties getProperties() { return properties; } /** *

* Prints out the loaded properties to the log. Useful when the application loads up for the first time. *

*/ private void printProperties() { LOGGER.info("fleet.app.port : " + properties.getAppPort()); LOGGER.info("fleet.database.url : " + properties.getDatabaseProperties().getDatabaseUrl()); LOGGER.info("fleet.database.username : " + properties.getDatabaseProperties().getDatabaseUsername()); LOGGER.info("fleet.database.password : " + (showPasswords() ? properties.getDatabaseProperties().getDatabasePassword() : "***")); LOGGER.info("app.version : " + getProperties().getVersionProperties()); } /** *

* Ideally in a production environment you don't want to log passwords or keys. *

* * @return * true if passwords can be logged to the terminal. */ private boolean showPasswords() { return FleetRuntime.SHOW_PASSWORDS; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/ServiceProvider.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core; import io.linuxserver.fleet.v2.file.FileManager; import io.linuxserver.fleet.v2.service.ImageService; import io.linuxserver.fleet.v2.service.ScheduleService; import io.linuxserver.fleet.v2.service.SynchronisationService; import io.linuxserver.fleet.v2.service.UserService; public interface ServiceProvider { SynchronisationService getSynchronisationService(); ImageService getImageService(); ScheduleService getScheduleService(); UserService getUserService(); FileManager getFileManager(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/config/AppProperties.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core.config; import io.linuxserver.fleet.core.FleetRuntime; import io.linuxserver.fleet.v2.client.docker.dockerhub.DockerHubCredentials; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Properties; public class AppProperties { private Properties properties; public AppProperties(final Properties properties) { this.properties = properties; } public DatabaseConnectionProperties getDatabaseProperties() { return new DatabaseConnectionProperties(getDatabaseDriverClassName(), getDatabaseUrl(), getDatabaseUsername(), getDatabasePassword()); } public final VersionProperties getVersionProperties() { return new VersionProperties(getStringProperty("app.version"), getStringProperty("app.build.user"), getStringProperty("app.build.date"), getStringProperty("app.build.os")); } private String getDatabaseDriverClassName() { return getStringProperty("fleet.database.driver"); } private String getDatabaseUrl() { return getStringProperty("fleet.database.url"); } private String getDatabaseUsername() { return getStringProperty("fleet.database.username"); } private String getDatabasePassword() { return getStringProperty("fleet.database.password"); } public final Path getStaticFilesPath() { return Paths.get(FleetRuntime.CONFIG_BASE, getStringProperty("fleet.static.dirname")).toAbsolutePath(); } public String getAppSecret() { String secret = getStringProperty("fleet.admin.secret"); return null == secret ? "" : secret; } public int getAppPort() { return Integer.parseInt(getStringProperty("fleet.app.port")); } public boolean isDockerHubAuthEnabled() { return "true".equalsIgnoreCase(getStringProperty("fleet.dockerhub.auth.enabled")); } public DockerHubCredentials getDockerHubCredentials() { final String username = getStringProperty("fleet.dockerhub.username"); final String password = getStringProperty("fleet.dockerhub.password"); return new DockerHubCredentials(username, password); } /** *

* Obtains the property value from three separate sources: first from the config file. If not present, it will look * at the JVM runtime. If that is not present, it will finally check the system environment. *

*/ private String getStringProperty(String propertyKey) { String property = properties.getProperty(propertyKey); if (null == property) { property = System.getProperty(propertyKey); if (null == property) { property = System.getenv(propertyKey.replace(".", "_")); } } return property; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/config/DatabaseConnectionProperties.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core.config; public class DatabaseConnectionProperties { private final String driverClass; private final String url; private final String username; private final String password; public DatabaseConnectionProperties(final String driverClass, final String url, final String username, final String password) { this.driverClass = driverClass; this.url = url; this.username = username; this.password = password; } public final String getDatabaseDriverClass() { return driverClass; } public final String getDatabaseUrl() { return url; } public final String getDatabaseUsername() { return username; } public final String getDatabasePassword() { return password; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/config/Version.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core.config; public class Version { private int major; private int minor; private int patch; public Version(int major, int minor, int patch) { this.major = major; this.minor = minor; this.patch = patch; } public Version(String version) { String[] bits = version.split("\\."); this.major = Integer.parseInt(bits[0]); this.minor = Integer.parseInt(bits[1]); this.patch = Integer.parseInt(bits[2]); } public int getMajor() { return major; } public int getMinor() { return minor; } public int getPatch() { return patch; } public boolean isNewerThan(Version version) { if (this.major > version.major) { return true; } if (this.minor > version.minor) { return true; } else if (this.minor < version.minor) { return false; } return this.patch > version.patch; } @Override public String toString() { return major + "." + minor + "." + patch; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/config/VersionProperties.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core.config; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class VersionProperties { private final Version version; private final String buildUser; private final LocalDateTime buildDate; private final String buildPlatform; public VersionProperties(final String version, final String buildUser, final String buildDate, final String buildPlatform) { this.version = new Version(version); this.buildUser = buildUser; this.buildDate = LocalDateTime.parse(buildDate, DateTimeFormatter.ISO_DATE_TIME); this.buildPlatform = buildPlatform; } public final Version getVersion() { return version; } public final String getBuildUser() { return buildUser; } public final LocalDateTime getBuildDate() { return buildDate; } public final String getBuildPlatform() { return buildPlatform; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/config/WebConfiguration.java ================================================ /* * Copyright (c) 2019 Wallett * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core.config; public class WebConfiguration { private final AppProperties appProperties; public WebConfiguration(final AppProperties properties) { appProperties = properties; } public final int getPort() { return appProperties.getAppPort(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/db/DatabaseConnection.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core.db; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public interface DatabaseConnection { DataSource getDataSource(); Connection getConnection() throws SQLException; } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/db/DatabaseProvider.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core.db; import io.linuxserver.fleet.db.migration.DatabaseVersion; public interface DatabaseProvider { DatabaseConnection getDatabaseConnection(); DatabaseVersion getVersionHandler(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/core/db/DefaultDatabaseProvider.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.core.db; import io.linuxserver.fleet.db.migration.DatabaseVersion; public class DefaultDatabaseProvider implements DatabaseProvider { private final DatabaseConnection databaseConnection; private final DatabaseVersion databaseVersion; public DefaultDatabaseProvider(final DatabaseConnection databaseConnection) { this.databaseConnection = databaseConnection; this.databaseVersion = new DatabaseVersion(databaseConnection); } @Override public DatabaseConnection getDatabaseConnection() { return databaseConnection; } @Override public DatabaseVersion getVersionHandler() { return databaseVersion; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/db/DefaultDatabaseConnection.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.db; import io.linuxserver.fleet.core.config.DatabaseConnectionProperties; public class DefaultDatabaseConnection extends PoolingDatabaseConnection { public DefaultDatabaseConnection(final DatabaseConnectionProperties properties) { super(properties); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/db/PoolingDatabaseConnection.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public LicensedatabaseConnection.getDataSource( * along with this program. If not, see . */ package io.linuxserver.fleet.db; import com.zaxxer.hikari.HikariDataSource; import io.linuxserver.fleet.core.config.DatabaseConnectionProperties; import io.linuxserver.fleet.core.db.DatabaseConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public abstract class PoolingDatabaseConnection implements DatabaseConnection { private static final Logger LOGGER = LoggerFactory.getLogger(PoolingDatabaseConnection.class); private final HikariDataSource dataSource; PoolingDatabaseConnection(final DatabaseConnectionProperties properties) { dataSource = new HikariDataSource(); dataSource.setDriverClassName(properties.getDatabaseDriverClass()); dataSource.setJdbcUrl(properties.getDatabaseUrl()); dataSource.setUsername(properties.getDatabaseUsername()); dataSource.setPassword(properties.getDatabasePassword()); LOGGER.info("DataSource established: " + dataSource.getJdbcUrl()); } @Override public DataSource getDataSource() { return dataSource; } @Override public final Connection getConnection() throws SQLException { return dataSource.getConnection(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/db/dao/Utils.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.db.dao; import java.sql.CallableStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.sql.Types; import java.time.LocalDateTime; class Utils { static void setNullableInt(CallableStatement call, int position, Integer value) throws SQLException { if (null == value) call.setNull(position, Types.INTEGER); else call.setInt(position, value); } static void setNullableLong(CallableStatement call, int position, Long value) throws SQLException { if (null == value) call.setNull(position, Types.BIGINT); else call.setLong(position, value); } static void setNullableString(CallableStatement call, int position, String value) throws SQLException { if (null == value) call.setNull(position, Types.VARCHAR); else call.setString(position, value); } static void setNullableTimestamp(CallableStatement call, int position, LocalDateTime localDateTime) throws SQLException { if (null == localDateTime) { call.setNull(position, Types.TIMESTAMP); } else { call.setTimestamp(position, Timestamp.valueOf(localDateTime)); } } static void safeClose(CallableStatement call) { try { if (null != call) call.close(); } catch (SQLException e) { throw new RuntimeException("Unable to close call", e); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/db/migration/DatabaseVersion.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.db.migration; import io.linuxserver.fleet.core.FleetRuntime; import io.linuxserver.fleet.core.db.DatabaseConnection; import org.flywaydb.core.Flyway; import org.flywaydb.core.api.FlywayException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** *

* Manages versioning of the database to ensure it is kept up-to-date *

*/ public class DatabaseVersion { private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseVersion.class); private final Flyway flyway; public DatabaseVersion(final DatabaseConnection databaseConnection) { flyway = Flyway.configure().dataSource(databaseConnection.getDataSource()).load(); migrate(); } /** *

* Runs the migration process which runs any necessary scripts to get the database configured. *

*/ public void migrate() { try { if (FleetRuntime.NUKE_DATABASE) { flyway.clean(); } flyway.migrate(); } catch (FlywayException e) { LOGGER.error(e.getMessage()); throw new RuntimeException("Unable to start application because the database has gone out of sync.", e); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/db/query/InsertUpdateResult.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.db.query; public class InsertUpdateResult { private final T result; private final int status; private final String statusMessage; public InsertUpdateResult(T result) { this(result, InsertUpdateStatus.OK, "OK"); } public InsertUpdateResult(T result, int status, String statusMessage) { this.result = result; this.status = status; this.statusMessage = statusMessage; } public InsertUpdateResult(int status, String statusMessage) { this(null, status, statusMessage); } public final T getResult() { return result; } public final int getStatus() { return status; } public final String getStatusMessage() { return statusMessage; } public final boolean isError() { return status != InsertUpdateStatus.OK; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/db/query/InsertUpdateStatus.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.db.query; public interface InsertUpdateStatus { int OK = 0; int FAILED = 1; } ================================================ FILE: src/main/java/io/linuxserver/fleet/db/query/LimitOffset.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.db.query; public class LimitOffset { private final int limit; private final int offset; public LimitOffset(int limit, int offset) { this.limit = limit; this.offset = offset; } public int getLimit() { return limit; } public int getOffset() { return offset; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/db/query/LimitedResult.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.db.query; import java.util.List; public class LimitedResult { private final List results; private final LimitOffset next; private final int totalCount; public LimitedResult(List results, int totalCount) { this(results, totalCount,null); } public LimitedResult(List results, int totalCount, LimitOffset next) { this.results = results; this.totalCount = totalCount; this.next = next; } public List getResults() { return results; } public int getTotalCount() { return totalCount; } public LimitOffset getNext() { return next; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/DockerHubException.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub; public class DockerHubException extends RuntimeException { public DockerHubException(String message) { super(message); } public DockerHubException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/model/DockerHubV2Image.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.model; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @JsonInclude(JsonInclude.Include.NON_NULL) public class DockerHubV2Image { @JsonProperty("user") private String user; @JsonProperty("name") private String name; @JsonProperty("namespace") private String namespace; @JsonProperty("description") private String description; @JsonProperty("star_count") private int starCount; @JsonProperty("pull_count") private long pullCount; @JsonProperty("last_updated") private String lastUpdated; public final String getUser() { return user; } public final String getName() { return name; } public final String getNamespace() { return namespace; } public String getDescription() { return description; } public final int getStarCount() { return starCount; } public final long getPullCount() { return pullCount; } public final String getLastUpdated() { return lastUpdated; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/model/DockerHubV2ImageListResult.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.model; public class DockerHubV2ImageListResult extends DockerHubV2ScanResult { } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/model/DockerHubV2NamespaceLookupResult.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.model; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.ArrayList; import java.util.List; public class DockerHubV2NamespaceLookupResult { @JsonProperty("namespaces") private List namespaces = new ArrayList<>(); public List getNamespaces() { return namespaces; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/model/DockerHubV2ScanResult.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.model; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; public class DockerHubV2ScanResult { @JsonProperty("count") private int count; @JsonProperty("next") private String next; @JsonProperty("previous") private String previous; @JsonProperty("results") private List results; public final int getCount() { return count; } public final String getNext() { return next; } public final String getPrevious() { return previous; } public final List getResults() { return results; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/model/DockerHubV2Tag.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.model; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import java.util.Objects; public class DockerHubV2Tag { @JsonProperty("name") private String name; @JsonProperty("full_size") private long fullSize; @JsonProperty("last_updated") private String lastUpdated; @JsonProperty("images") private List images; public String getName() { return name; } public long getFullSize() { return fullSize; } public String getLastUpdated() { return lastUpdated; } public List getImages() { return images; } @Override public String toString() { return name; } @Override public int hashCode() { return Objects.hash(name, fullSize); } @Override public boolean equals(Object other) { if (!(other instanceof DockerHubV2Tag)) { return false; } if (other == this) { return true; } DockerHubV2Tag otherTag = (DockerHubV2Tag) other; return name.equals(otherTag.name) && fullSize == otherTag.fullSize; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/model/DockerHubV2TagDigest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.model; import com.fasterxml.jackson.annotation.JsonProperty; public class DockerHubV2TagDigest { @JsonProperty("size") private long size; @JsonProperty("digest") private String digest; @JsonProperty("architecture") private String architecture; @JsonProperty("variant") private String variant; public long getSize() { return size; } public String getDigest() { return digest; } public String getArchitecture() { return architecture; } public String getVariant() { return variant; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/model/DockerHubV2TagListResult.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.model; public class DockerHubV2TagListResult extends DockerHubV2ScanResult { } ================================================ FILE: src/main/java/io/linuxserver/fleet/dockerhub/util/DockerTagFinder.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.util; import io.linuxserver.fleet.v2.types.docker.DockerTag; import io.linuxserver.fleet.v2.types.docker.DockerTagManifestDigest; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; public class DockerTagFinder { public static DockerTag findVersionedTagMatchingBranch(List tags, String namedBranch) { Optional tagBranchName = tags.stream().filter(tag -> namedBranch.equals(tag.getName())).findFirst(); if (tagBranchName.isPresent()) { DockerTag namedTagForBranch = tagBranchName.get(); Optional versionedLatestTag = tags.stream() .filter(tag -> !tag.equals(namedTagForBranch) && allManifestsMatch(namedTagForBranch, tag)).findFirst(); return versionedLatestTag.orElse(namedTagForBranch); } return tags.isEmpty() ? null : tags.get(0); } private static boolean allManifestsMatch(final DockerTag namedTag, final DockerTag toCheck) { final List namedDigests = namedTag.getDigests(); final List digestsToCheck = toCheck.getDigests(); boolean allMatch = true; if (namedDigests.size() == digestsToCheck.size()) { final Map namedDigestsAsMap = toMapKeyedByArch(namedDigests); for (DockerTagManifestDigest digestToCheck : digestsToCheck) { final String archPlusVariant = digestToCheck.getArchitecture() + digestToCheck.getArchVariant(); final String foundDigest = namedDigestsAsMap.get(archPlusVariant); allMatch = allMatch && (null != foundDigest) && foundDigest.equals(digestToCheck.getDigest()); } } else { allMatch = false; } return allMatch; } private static Map toMapKeyedByArch(final List initialList) { final Map map = new HashMap<>(); for (DockerTagManifestDigest digest : initialList) { map.put(digest.getArchitecture() + digest.getArchVariant(), digest.getDigest()); } return map; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/exception/SaveException.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.exception; public class SaveException extends Exception { public SaveException(String message) { super(message); } public SaveException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/LoggerOwner.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2; import org.slf4j.Logger; public interface LoggerOwner { Logger getLogger(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/Utils.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2; public final class Utils { public static T ensureNotNull(final T obj) { if (null == obj) { throw new IllegalArgumentException("Parameter null"); } return obj; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/cache/AbstractItemCache.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.cache; import io.linuxserver.fleet.v2.key.HasKey; import io.linuxserver.fleet.v2.key.Key; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; public abstract class AbstractItemCache> implements ItemCache { private final Logger LOGGER = LoggerFactory.getLogger(getClass()); private final List> listeners; private final Map items; public AbstractItemCache() { listeners = new ArrayList<>(); items = new HashMap<>(); } public final void clear() { LOGGER.info("Emptying cache"); items.clear(); } public final void registerCacheListener(final ItemCacheListener listener) { LOGGER.info("Registering new cache listener {}", listener); listeners.add(listener); } @Override public final void addItem(final ITEM item) { final ITEM original = items.get(item.getKey()); final ITEM cached = items.put(item.getKey(), item); LOGGER.info("Item {} cached", item); if (null == original) { listeners.forEach(l -> l.onItemAdded(cached)); } else { listeners.forEach(l -> l.onItemUpdated(original, cached)); } } @Override public boolean isEmpty() { return items.isEmpty(); } @Override public final ITEM findItem(final KEY key) { return items.get(key); } @Override public final void removeItem(final KEY key) { final ITEM removed = items.remove(key); LOGGER.info("Item {} removed from cache", removed); listeners.forEach(l -> l.onItemRemoved(removed)); } @Override public final boolean isItemCached(final KEY key) { return items.containsKey(key); } @Override public Collection getAllItems() { return items.values(); } @Override public final void addAllItems(Collection allItems) { allItems.forEach(this::addItem); } @Override public int size() { return items.size(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/cache/BasicItemCache.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.cache; import io.linuxserver.fleet.v2.key.HasKey; import io.linuxserver.fleet.v2.key.Key; public final class BasicItemCache> extends AbstractItemCache { // Default implementation } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/cache/ImageCache.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.cache; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.types.Image; public class ImageCache extends AbstractItemCache { } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/cache/ItemCache.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.cache; import io.linuxserver.fleet.v2.key.HasKey; import io.linuxserver.fleet.v2.key.Key; import java.util.Collection; public interface ItemCache> { boolean isEmpty(); void addItem(ITEM item); ITEM findItem(KEY key); void removeItem(KEY key); boolean isItemCached(KEY key); Collection getAllItems(); void addAllItems(Collection items); int size(); interface ItemCacheListener { void onItemAdded(final ITEM item); void onItemUpdated(final ITEM oldItem, final ITEM newItem); void onItemRemoved(final ITEM item); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/cache/RepositoryCache.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.cache; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.ImageLookupKey; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.Repository; public class RepositoryCache extends AbstractItemCache { public final Image lookupImage(final ImageLookupKey lookupKey) { for (Repository repository : getAllItems()) { for (Image image : repository.getImages()) { if (lookupKey.isLookupKeyFor(image)) { return image; } } } return null; } public final Image findImage(final ImageKey imageKey) { if (isItemCached(imageKey.getRepositoryKey())) { final Repository repository = findItem(imageKey.getRepositoryKey()); for (Image image : repository.getImages()) { if (imageKey.equals(image.getKey())) { return image; } } } return null; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/cache/ScheduleCache.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.cache; import io.linuxserver.fleet.v2.key.ScheduleKey; import io.linuxserver.fleet.v2.thread.schedule.AppSchedule; public class ScheduleCache extends AbstractItemCache { public final boolean isScheduleRunning(final ScheduleKey scheduleKey) { return isItemCached(scheduleKey); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/AbstractDockerApiClient.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker; import io.linuxserver.fleet.v2.client.docker.converter.DockerResponseConverter; import io.linuxserver.fleet.v2.types.docker.DockerImage; import io.linuxserver.fleet.v2.types.docker.DockerTag; import java.util.List; import java.util.stream.Collectors; public abstract class AbstractDockerApiClient, TC extends DockerResponseConverter> implements DockerApiClient { private final IC imageConverter; private final TC tagConverter; public AbstractDockerApiClient(final IC imageConverter, final TC tagConverter) { this.imageConverter = imageConverter; this.tagConverter = tagConverter; } @Override public final DockerImage fetchImage(String imageName) { final D dockerModel = fetchImageFromApi(imageName); if (null == dockerModel) { return null; } return imageConverter.convert(dockerModel); } @Override public final List fetchAllImages(String repositoryName) { return fetchAllImagesFromApi(repositoryName).stream().map(imageConverter::convert).collect(Collectors.toList()); } @Override public final List fetchImageTags(String imageName) { return fetchTagsFromApi(imageName).stream().map(tagConverter::convert).collect(Collectors.toList()); } protected abstract D fetchImageFromApi(final String imageName); protected abstract List fetchAllImagesFromApi(final String repositoryName); protected abstract List fetchTagsFromApi(final String imageName); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/DockerApiClient.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker; import io.linuxserver.fleet.v2.types.docker.DockerImage; import io.linuxserver.fleet.v2.types.docker.DockerTag; import java.util.List; public interface DockerApiClient { boolean isRepositoryValid(final String repositoryName); DockerImage fetchImage(final String imageName); List fetchAllImages(final String repositoryName); List fetchImageTags(final String imageName); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/DockerImageNotFoundException.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker; public class DockerImageNotFoundException extends RuntimeException { public DockerImageNotFoundException(final String reason) { super(reason); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/converter/AbstractDockerResponseConverter.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.converter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.LocalDateTime; import java.time.format.DateTimeParseException; public abstract class AbstractDockerResponseConverter implements DockerResponseConverter { private final Logger LOGGER = LoggerFactory.getLogger(getClass()); @Override public final I convert(final D dockerModel) { try { if (null == dockerModel) { LOGGER.warn("Attempted to convert null image"); } else { return doPlainConvert(dockerModel); } } catch (Exception e) { LOGGER.error("Unable to convert docker model to internal.", e); } return null; } protected abstract I doPlainConvert(final D dockerApiImage); protected final LocalDateTime parseDockerHubDate(String date) { if (null == date) { return null; } try { final String dateToParse = (date.endsWith("Z") ? date.substring(0, date.length() - 1) : date); return LocalDateTime.parse(dateToParse); } catch (DateTimeParseException e) { LOGGER.error("parseDockerHubDate(" + date + ") unable to leniently parse date.", e); return null; } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/converter/DockerResponseConverter.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.converter; public interface DockerResponseConverter { INTERNAL_MODEL convert(final DOCKER_MODEL dockerModel); Class getConverterClass(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/dockerhub/DockerHubApiClient.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.dockerhub; import io.linuxserver.fleet.dockerhub.DockerHubException; import io.linuxserver.fleet.dockerhub.model.DockerHubV2Image; import io.linuxserver.fleet.dockerhub.model.DockerHubV2ImageListResult; import io.linuxserver.fleet.dockerhub.model.DockerHubV2Tag; import io.linuxserver.fleet.dockerhub.model.DockerHubV2TagListResult; import io.linuxserver.fleet.v2.Utils; import io.linuxserver.fleet.v2.client.docker.AbstractDockerApiClient; import io.linuxserver.fleet.v2.client.rest.HttpException; import io.linuxserver.fleet.v2.client.rest.RestClient; import io.linuxserver.fleet.v2.client.rest.RestResponse; import java.util.ArrayList; import java.util.List; public class DockerHubApiClient extends AbstractDockerApiClient { public static final String DockerHubApiUrl = "https://hub.docker.com/v2"; private static final int DefaultPageSize = 1000; private final RestClient restClient; private final IDockerHubAuthenticator authenticator; public DockerHubApiClient(final RestClient restClient, final IDockerHubAuthenticator authenticator) { super(new DockerHubImageConverter(), new DockerHubTagConverter()); this.restClient = Utils.ensureNotNull(restClient); this.authenticator = Utils.ensureNotNull(authenticator); } @Override public final boolean isRepositoryValid(String repositoryName) { try { return !fetchAllImages(repositoryName).isEmpty(); } catch (HttpException e) { throw new DockerHubException("Unable to verify repository " + repositoryName, e); } } @Override protected final DockerHubV2Image fetchImageFromApi(String imageName) { try { final String absoluteUrl = DockerHubApiUrl + "/repositories/" + imageName + "/"; final RestResponse restResponse = doCall(absoluteUrl, DockerHubV2Image.class); if (isResponseOK(restResponse)) { return restResponse.getPayload(); } return null; } catch (HttpException e) { throw new DockerHubException("Unable to get images for " + imageName, e); } } @Override protected final List fetchAllImagesFromApi(String repositoryName) { final List images = new ArrayList<>(); try { String url = DockerHubApiUrl + "/repositories/" + repositoryName + "/?page_size=" + DefaultPageSize; while (url != null) { final RestResponse response = doCall(url, DockerHubV2ImageListResult.class); if (isResponseOK(response)) { DockerHubV2ImageListResult payload = response.getPayload(); images.addAll(payload.getResults()); url = payload.getNext(); } } return images; } catch (HttpException e) { throw new DockerHubException("Unable to get images for " + repositoryName, e); } } @Override protected final List fetchTagsFromApi(String imageName) { try { List tags = new ArrayList<>(); String absoluteUrl = DockerHubApiUrl + "/repositories/" + imageName + "/tags/?page_size=" + DefaultPageSize; while (absoluteUrl != null) { final RestResponse response = doCall(absoluteUrl, DockerHubV2TagListResult.class); if (isResponseOK(response)) { final DockerHubV2TagListResult payload = response.getPayload(); tags.addAll(payload.getResults()); absoluteUrl = payload.getNext(); } } return tags; } catch (HttpException e) { throw new DockerHubException("Unable to get tags for " + imageName, e); } } /** *

* Attempts to call DockerHub with the currently set credentials. If they have expired, it will refresh them and try again. * This process will only try again once, so if the refresh resulted in another stale token, it will need to be handled. *

*/ private RestResponse doCall(String url, Class responseType) { RestResponse restResponse = restClient.executeGet(url, null, authenticator.buildAuthHeaders(), responseType); if (isResponseUnauthorised(restResponse)) { authenticator.refreshToken(); restResponse = restClient.executeGet(url, null, authenticator.buildAuthHeaders(), responseType); } return restResponse; } private boolean isResponseOK(final RestResponse restResponse) { return restResponse.getStatusCode() == 200; } private boolean isResponseUnauthorised(RestResponse restResponse) { return restResponse.getStatusCode() == 401; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/dockerhub/DockerHubAuthenticator.java ================================================ package io.linuxserver.fleet.v2.client.docker.dockerhub; import io.linuxserver.fleet.dockerhub.DockerHubException; import io.linuxserver.fleet.v2.client.rest.RestClient; import io.linuxserver.fleet.v2.client.rest.RestResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; public class DockerHubAuthenticator implements IDockerHubAuthenticator { private static final Logger LOGGER = LoggerFactory.getLogger(DockerHubAuthenticator.class); private final RestClient client; private final DockerHubCredentials credentials; private String token; public DockerHubAuthenticator(final DockerHubCredentials credentials, final RestClient client) { this.credentials = credentials; this.client = client; refreshToken(); } /** *

* Re-authenticates with Docker Hub to obtain a fresh JWT. *

* * @return * The new JWT to be used in authenticated requests. */ public synchronized String refreshToken() { LOGGER.info("Refreshing token for Docker Hub authentication"); final RestResponse authenticationResponse = client.executePost( DockerHubApiClient.DockerHubApiUrl + "/users/login", null, null, credentials, DockerHubTokenResponse.class); if (authenticationResponse.getStatusCode() == 200) { LOGGER.info("Refresh successful"); final String token = authenticationResponse.getPayload().getToken(); this.token = token; return token; } LOGGER.info("Unable to refresh token."); throw new DockerHubException("Unable to authenticate with Docker Hub. Check credentials"); } synchronized String getCurrentToken() { if (null == token) { return refreshToken(); } return token; } @Override public Map buildAuthHeaders() { final Map authHeaders = new HashMap<>(); authHeaders.put("Authorization", "JWT " + getCurrentToken()); return authHeaders; } static class DockerHubTokenResponse { private String token; public String getToken() { return token; } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/dockerhub/DockerHubCredentials.java ================================================ package io.linuxserver.fleet.v2.client.docker.dockerhub; import io.linuxserver.fleet.v2.Utils; public class DockerHubCredentials { private final String username; private final String password; public DockerHubCredentials(final String username, final String password) { this.username = Utils.ensureNotNull(username); this.password = Utils.ensureNotNull(password); } public final String getUsername() { return username; } public final String getPassword() { return password; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/dockerhub/DockerHubImageConverter.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.dockerhub; import io.linuxserver.fleet.dockerhub.model.DockerHubV2Image; import io.linuxserver.fleet.v2.client.docker.converter.AbstractDockerResponseConverter; import io.linuxserver.fleet.v2.types.docker.DockerImage; public class DockerHubImageConverter extends AbstractDockerResponseConverter { @Override protected final DockerImage doPlainConvert(final DockerHubV2Image dockerApiImage) { return new DockerImage(dockerApiImage.getName(), dockerApiImage.getNamespace(), dockerApiImage.getDescription(), dockerApiImage.getStarCount(), dockerApiImage.getPullCount(), parseDockerHubDate(dockerApiImage.getLastUpdated())); } @Override public Class getConverterClass() { return DockerHubV2Image.class; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/dockerhub/DockerHubTagConverter.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.dockerhub; import io.linuxserver.fleet.dockerhub.model.DockerHubV2Tag; import io.linuxserver.fleet.dockerhub.model.DockerHubV2TagDigest; import io.linuxserver.fleet.v2.client.docker.converter.AbstractDockerResponseConverter; import io.linuxserver.fleet.v2.types.docker.DockerTag; import io.linuxserver.fleet.v2.types.docker.DockerTagManifestDigest; public class DockerHubTagConverter extends AbstractDockerResponseConverter { @Override protected final DockerTag doPlainConvert(final DockerHubV2Tag dockerApiImage) { final DockerTag dockerTag = new DockerTag(dockerApiImage.getName(), dockerApiImage.getFullSize(), parseDockerHubDate(dockerApiImage.getLastUpdated())); for (DockerHubV2TagDigest tagImageDigest : dockerApiImage.getImages()) { dockerTag.addDigest(new DockerTagManifestDigest(tagImageDigest.getSize(), tagImageDigest.getDigest(), tagImageDigest.getArchitecture(), tagImageDigest.getVariant())); } return dockerTag; } @Override public Class getConverterClass() { return DockerHubV2Tag.class; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/dockerhub/IDockerHubAuthenticator.java ================================================ package io.linuxserver.fleet.v2.client.docker.dockerhub; import java.util.Map; public interface IDockerHubAuthenticator { Map buildAuthHeaders(); String refreshToken(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/dockerhub/NoOpDockerHubAuthenticator.java ================================================ package io.linuxserver.fleet.v2.client.docker.dockerhub; import java.util.HashMap; import java.util.Map; public class NoOpDockerHubAuthenticator implements IDockerHubAuthenticator { @Override public Map buildAuthHeaders() { return new HashMap<>(); } @Override public String refreshToken() { return null; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/github/GitHubContainerRegistryClient.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.github; import io.linuxserver.fleet.v2.client.docker.AbstractDockerApiClient; import io.linuxserver.fleet.v2.client.docker.github.model.GitHubImage; import io.linuxserver.fleet.v2.client.docker.github.model.GitHubTag; import java.util.List; public class GitHubContainerRegistryClient extends AbstractDockerApiClient { public GitHubContainerRegistryClient() { super(new GitHubImageConverter(), new GitHubTagConverter()); } @Override protected final GitHubImage fetchImageFromApi(String imageName) { return null; } @Override protected final List fetchAllImagesFromApi(String repositoryName) { return null; } @Override protected final List fetchTagsFromApi(String imageName) { return null; } @Override public final boolean isRepositoryValid(String repositoryName) { return false; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/github/GitHubImageConverter.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.github; import io.linuxserver.fleet.v2.client.docker.converter.AbstractDockerResponseConverter; import io.linuxserver.fleet.v2.client.docker.github.model.GitHubImage; import io.linuxserver.fleet.v2.types.docker.DockerImage; public class GitHubImageConverter extends AbstractDockerResponseConverter { @Override protected final DockerImage doPlainConvert(final GitHubImage dockerApiImage) { return null; } @Override public final Class getConverterClass() { return GitHubImage.class; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/github/GitHubTagConverter.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.github; import io.linuxserver.fleet.v2.client.docker.converter.AbstractDockerResponseConverter; import io.linuxserver.fleet.v2.client.docker.github.model.GitHubTag; import io.linuxserver.fleet.v2.types.docker.DockerTag; public class GitHubTagConverter extends AbstractDockerResponseConverter { @Override protected final DockerTag doPlainConvert(final GitHubTag dockerApiImage) { return null; } @Override public final Class getConverterClass() { return GitHubTag.class; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/github/model/GitHubImage.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.github.model; public class GitHubImage { } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/github/model/GitHubTag.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.github.model; public class GitHubTag { } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/queue/AsyncDockerApiRequest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.queue; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.thread.AsyncTask; public interface AsyncDockerApiRequest extends AsyncTask { ImageKey getImageKey(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/queue/AsyncDockerApiResponse.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.queue; import io.linuxserver.fleet.v2.thread.AsyncTaskResponse; public interface AsyncDockerApiResponse extends AsyncTaskResponse { void handleDockerApiResponse(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerApiDelegate.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.queue; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.Utils; import io.linuxserver.fleet.v2.client.docker.DockerApiClient; import io.linuxserver.fleet.v2.client.docker.DockerImageNotFoundException; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.thread.AsyncTaskDelegate; import io.linuxserver.fleet.v2.types.docker.DockerImage; import io.linuxserver.fleet.v2.types.docker.DockerTag; import java.util.List; public class DockerApiDelegate implements AsyncTaskDelegate { private final FleetAppController controller; private final DockerApiClient apiClient; public DockerApiDelegate(final FleetAppController controller, final DockerApiClient dockerApiClient) { this.controller = controller; this.apiClient = Utils.ensureNotNull(dockerApiClient); } public final boolean isRepositoryValid(final String repositoryName) { return apiClient.isRepositoryValid(repositoryName); } public final List getImagesForRepository(final RepositoryKey repositoryKey) { return apiClient.fetchAllImages(repositoryKey.getName()); } public final DockerImage getCurrentImageView(final ImageKey imageKey) { final DockerImage dockerImage = apiClient.fetchImage(imageKey.getAsRepositoryAndImageName()); if (null == dockerImage) { throw new DockerImageNotFoundException("Image " + imageKey.getAsRepositoryAndImageName() + " was not found upstream."); } final List allImageTags = apiClient.fetchImageTags(imageKey.getAsRepositoryAndImageName()); allImageTags.forEach(dockerImage::addTag); return dockerImage; } @Override public FleetAppController getController() { return controller; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerApiTaskConsumer.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.queue; import io.linuxserver.fleet.v2.service.SynchronisationService; import io.linuxserver.fleet.v2.thread.AbstractTaskQueueConsumer; import io.linuxserver.fleet.v2.thread.TaskExecutionException; public final class DockerApiTaskConsumer extends AbstractTaskQueueConsumer { public DockerApiTaskConsumer(final SynchronisationService syncService) { super(syncService.getController(), syncService.getConfiguredDockerDelegate(), syncService.getSyncQueue(), "DockerSyncConsumer"); } @Override protected void handleTaskResponse(final DockerImageUpdateResponse response) { try { response.handleDockerApiResponse(); } catch (Exception e) { getLogger().error("handleTaskResponse caught unhandled error, but not something worthy of stalling thread", e); throw new TaskExecutionException(e); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerImageMissingUpdateResponse.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.queue; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.ImageKey; public class DockerImageMissingUpdateResponse extends DockerImageUpdateResponse { public DockerImageMissingUpdateResponse(final FleetAppController controller, final ImageKey imageKey) { super(controller, imageKey, null); } @Override public final void handleDockerApiResponse() { // Do nothing. Let schedule handle this. } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerImageUpdateRequest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.queue; import io.linuxserver.fleet.v2.client.docker.DockerImageNotFoundException; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.thread.AbstractAppTask; public class DockerImageUpdateRequest extends AbstractAppTask { private final ImageKey imageKey; public DockerImageUpdateRequest(final ImageKey imageKey) { super(imageKey.toString()); this.imageKey = imageKey; } @Override protected DockerImageUpdateResponse performTaskInternal(final DockerApiDelegate delegate) { try { return new DockerImageUpdateResponse(delegate.getController(), imageKey, delegate.getCurrentImageView(imageKey)); } catch (DockerImageNotFoundException e) { getLogger().warn("Request responded with an empty response so assuming image {} has been removed upstream. Error message: {}", imageKey, e.getMessage()); return new DockerImageMissingUpdateResponse(delegate.getController(), imageKey); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/queue/DockerImageUpdateResponse.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.queue; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.types.docker.DockerImage; public class DockerImageUpdateResponse implements AsyncDockerApiResponse { private final FleetAppController controller; private final ImageKey imageKey; private final DockerImage latestImage; public DockerImageUpdateResponse(final FleetAppController controller, final ImageKey imageKey, final DockerImage latestImage) { this.controller = controller; this.imageKey = imageKey; this.latestImage = latestImage; } protected final FleetAppController getController() { return controller; } @Override public void handleDockerApiResponse() { controller.getImageService().applyImageUpstreamUpdate(imageKey, latestImage); } @Override public void handleResponse() { handleDockerApiResponse(); } @Override public String toString() { return "DockerImageUpdateResponse[" + imageKey.toString() + "]"; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/docker/queue/TaskQueue.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.docker.queue; import io.linuxserver.fleet.v2.thread.AsyncTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class TaskQueue> { private static final Logger LOGGER = LoggerFactory.getLogger(TaskQueue.class); private final BlockingQueue activeTaskQueue; public TaskQueue() { activeTaskQueue = new LinkedBlockingQueue<>(); } public final boolean submitTask(final TASK task) { LOGGER.info("Task submitted: {}", task); if (activeTaskQueue.contains(task)) { LOGGER.warn("Task {} is already queued so will not duplicate the request.", task); return false; } return activeTaskQueue.add(task); } public final int size() { return activeTaskQueue.size(); } public final TASK retrieveNextTask() throws InterruptedException { return activeTaskQueue.take(); } public final boolean isEmpty() { return activeTaskQueue.isEmpty(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/rest/HttpException.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.rest; public class HttpException extends RuntimeException { public HttpException(String message) { super(message); } public HttpException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/rest/RestClient.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.rest; import io.linuxserver.fleet.v2.client.rest.marshalling.JacksonMarshallingStrategy; import io.linuxserver.fleet.v2.client.rest.marshalling.MarshallingStrategy; import io.linuxserver.fleet.v2.client.rest.proxy.LazyLoadPayloadProxy; import org.apache.http.HttpEntity; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Map; /** *

* Simple wrapper for base HttpClient, providing an easier way to marshall/unmarshall payloads *

*/ public class RestClient { private static final Logger LOGGER = LoggerFactory.getLogger(RestClient.class); private final CloseableHttpClient client; private final HttpClientContext clientContext; private MarshallingStrategy marshallingStrategy; public RestClient() { setMarshallingStrategy(new JacksonMarshallingStrategy()); client = HttpClients.custom().setConnectionManager(new PoolingHttpClientConnectionManager()).build(); clientContext = HttpClientContext.create(); } private void setMarshallingStrategy(MarshallingStrategy marshallingStrategy) { LOGGER.debug("Configuring RestClient with " + marshallingStrategy.getClass().getName()); this.marshallingStrategy = marshallingStrategy; } public RestResponse executeGet(String url, Map queryParameters, Map headers, Class responseType) { try { return executeBaseRequest(responseType, headers, new HttpGet(url + parseQueryParameters(queryParameters))); } catch (IOException e) { LOGGER.error("Unable to perform GET", e); throw new HttpException("Unable to perform GET", e); } } public RestResponse executePost(String url, Map queryParameters, Map headers, Object payload, Class responseType) { try { HttpPost post = new HttpPost(url + parseQueryParameters(queryParameters)); post.setEntity(new StringEntity(marshallingStrategy.marshall(payload), StandardCharsets.UTF_8)); post.setHeader("Content-Type", marshallingStrategy.getContentType()); return executeBaseRequest(responseType, headers, post); } catch (IOException e) { LOGGER.error("Unable to perform GET", e); throw new HttpException("Unable to perform GET", e); } } private RestResponse executeBaseRequest(Class responseType, Map headers, HttpRequestBase request) throws IOException { LOGGER.debug("url : " + request.getURI().toString()); LOGGER.debug("headers : " + headers); if (headers != null) { for (Map.Entry header : headers.entrySet()) request.setHeader(header.getKey(), header.getValue()); } LOGGER.debug("Executing."); try (CloseableHttpResponse response = client.execute(request, clientContext)) { StatusLine statusLine = response.getStatusLine(); LOGGER.debug("Response status: " + statusLine); int statusCode = statusLine.getStatusCode(); HttpEntity content = response.getEntity(); String responseBody = null; if (null != content) { responseBody = EntityUtils.toString(content); LOGGER.debug("Parsed response payload: " + responseBody); } if (null != responseBody) return new RestResponse<>(new LazyLoadPayloadProxy<>(marshallingStrategy, responseBody, responseType), statusCode); return new RestResponse<>(statusCode); } finally { request.releaseConnection(); } } private String parseQueryParameters(Map queryParameters) { if (null != queryParameters) { StringBuilder builtParameterString = new StringBuilder("?"); for (Map.Entry param : queryParameters.entrySet()) builtParameterString.append(param.getKey()).append("=").append(param.getValue()).append("&"); builtParameterString.setLength(builtParameterString.length() - 1); return builtParameterString.toString(); } return ""; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/rest/RestResponse.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.rest; import io.linuxserver.fleet.v2.client.rest.proxy.PayloadProxy; public class RestResponse { private PayloadProxy payloadProxy; private int statusCode; RestResponse(int statusCode) { this(null, statusCode); } RestResponse(PayloadProxy payloadProxy, int statusCode) { this.payloadProxy = payloadProxy; this.statusCode = statusCode; } public T getPayload() { return payloadProxy.get(); } public int getStatusCode() { return statusCode; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/rest/marshalling/JacksonMarshallingStrategy.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.rest.marshalling; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; /** * Jackson JSON implementation of the marshalling strategy. This will convert incoming * and outgoing messages formatted in JSON. */ public class JacksonMarshallingStrategy implements MarshallingStrategy { private static final ObjectMapper OBJECT_MAPPER; static { OBJECT_MAPPER = new ObjectMapper(); OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } @Override public T unmarshall(String value, Class classType) throws IOException { return OBJECT_MAPPER.readValue(value, classType); } @Override public String marshall(Object value) throws JsonProcessingException { return OBJECT_MAPPER.writeValueAsString(value); } @Override public String getContentType() { return "application/json"; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/rest/marshalling/MarshallingStrategy.java ================================================ package io.linuxserver.fleet.v2.client.rest.marshalling; import java.io.IOException; public interface MarshallingStrategy { /** *

* Converts a given string value into its representative object type. *

* @param value * The value to convert to an object * @param classType * The object class definition * @return * The converted object */ T unmarshall(String value, Class classType) throws IOException; /** *

* Converts an object into a single representative string value. *

* @param value * The object to convert * @return * The result of the conversion */ String marshall(Object value) throws IOException; /** *

* The content type of the payloads represented by this strategy. *

*/ String getContentType(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/rest/proxy/LazyLoadPayloadProxy.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.rest.proxy; import io.linuxserver.fleet.v2.client.rest.HttpException; import io.linuxserver.fleet.v2.client.rest.marshalling.MarshallingStrategy; import java.io.IOException; public class LazyLoadPayloadProxy implements PayloadProxy { private final MarshallingStrategy marshallingStrategy; private final String payload; private final Class payloadType; public LazyLoadPayloadProxy(MarshallingStrategy marshallingStrategy, String payload, Class payloadType) { this.marshallingStrategy = marshallingStrategy; this.payload = payload; this.payloadType = payloadType; } @Override public T get() { try { return marshallingStrategy.unmarshall(payload, payloadType); } catch (IOException e) { throw new HttpException("Unable to unmarshall response payload", e); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/client/rest/proxy/PayloadProxy.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.client.rest.proxy; public interface PayloadProxy { T get(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/AbstractDAO.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import io.linuxserver.fleet.core.db.DatabaseProvider; import io.linuxserver.fleet.v2.LoggerOwner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.SQLException; public class AbstractDAO implements LoggerOwner { private final Logger logger = LoggerFactory.getLogger(getClass()); private final DatabaseProvider databaseProvider; public AbstractDAO(final DatabaseProvider databaseProvider) { this.databaseProvider = databaseProvider; } protected final Connection getConnection() throws SQLException { return databaseProvider.getDatabaseConnection().getConnection(); } @Override public final Logger getLogger() { return logger; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/DbUpdateStatus.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; public enum DbUpdateStatus { Inserted, Updated, NoChange, Exists; public final boolean isExpected(final DbUpdateStatus expected) { return this == expected; } public final boolean isExists() { return this == Exists; } public final boolean isInserted() { return this == Inserted; } public final boolean isUpdated() { return this == Updated; } public final boolean isNoChange() { return this == NoChange; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/DefaultImageDAO.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import io.linuxserver.fleet.core.db.DatabaseProvider; import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.db.query.InsertUpdateStatus; import io.linuxserver.fleet.v2.key.HasKey; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.key.TagBranchKey; import io.linuxserver.fleet.v2.types.*; import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest; import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest; import io.linuxserver.fleet.v2.types.internal.TagBranchOutlineRequest; import io.linuxserver.fleet.v2.types.meta.*; import io.linuxserver.fleet.v2.types.meta.history.ImagePullHistory; import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic; import java.sql.*; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class DefaultImageDAO extends AbstractDAO implements ImageDAO { private static final String GetRepositoryKeys = "{CALL Repository_GetRepositoryKeys()}"; private static final String GetRepository = "{CALL Repository_Get(?)}"; private static final String DeleteRepository = "{CALL Repository_Delete(?,?)}"; private static final String GetImageKeys = "{CALL Repository_GetImageKeys(?)}"; private static final String CreateRepositoryOutline = "{CALL Repository_CreateOutline(?,?,?,?,?,?,?,?)}"; private static final String StoreRepository = "{CALL Repository_Store(?,?,?,?)}"; private static final String StoreImage = "{CALL Image_Store(?,?,?,?,?,?,?,?,?,?,?)}"; private static final String CreateTagBranchOutline = "{CALL Image_CreateTagBranchOutline(?,?)}"; private static final String RemoveOrphanBranches = "{CALL Image_RemoveOrphanBranches(?,?)}"; private static final String StoreTagBranch = "{CALL Image_StoreTagBranch(?,?,?,?)}"; private static final String StoreTagDigest = "{CALL Image_StoreTagDigest(?,?,?,?,?)}"; private static final String GetTagBranches = "{CALL Image_GetTagBranches(?)}"; private static final String GetTagDigests = "{CALL Image_GetTagDigests(?)}"; private static final String CreateImageOutline = "{CALL Image_CreateOutline(?,?,?,?,?,?,?,?,?,?)}"; private static final String GetImage = "{CALL Image_Get(?)}"; private static final String DeleteImage = "{CALL Image_Delete(?)}"; private static final String GetImageStats = "{CALL Image_GetStats(?)}"; private static final String GetExternalUrls = "{CALL Image_GetExternalUrls(?)}"; private static final String RemoveOrphanUrls = "{CALL Image_RemoveOrphanUrls(?, ?)}"; private static final String StoreCoreMetaData = "{CALL Image_StoreCoreMetaData(?,?,?,?,?)}"; private static final String StoreExternalUrl = "{CALL Image_StoreExternalUrl(?,?,?,?,?)}"; private final ImageTemplateFactory templateFactory; public DefaultImageDAO(final DatabaseProvider databaseConnection) { super(databaseConnection); this.templateFactory = new ImageTemplateFactory(); } @Override public Image fetchImage(final ImageKey imageKey) { try (final Connection connection = getConnection()) { return makeImage(imageKey, connection); } catch (SQLException e) { getLogger().error("Error caught when executing SQL: fetchImage", e); throw new RuntimeException("fetchImage", e); } } @Override public InsertUpdateResult storeImage(final Image image) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(StoreImage)) { int i = 1; call.setInt(i++, image.getKey().getId()); call.setLong(i++, image.getPullCount()); call.setInt(i++, image.getStarCount()); Utils.setNullableString(call, i++, image.getDescription()); Utils.setNullableTimestamp(call, i++, image.getLastUpdated()); call.setBoolean(i++, image.isDeprecated()); call.setBoolean(i++, image.isHidden()); call.setBoolean(i++, image.isStable()); call.setBoolean(i++, image.isSyncEnabled()); Utils.setNullableString(call, i++, image.getVersionMask()); call.registerOutParameter(i, Types.VARCHAR); final ResultSet results = call.executeQuery(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(i)); if (status.isNoChange()) { getLogger().warn("storeImage attempted to update an image which did not exist in the database: {}", image); } else if (results.next()) { storeTagBranches(connection, image); return new InsertUpdateResult<>(makeImage(makeImageKey(results), connection)); } return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "storeImage did not return anything."); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: storeImage", e); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, e.getMessage()); } } @Override public InsertUpdateResult storeImageMetaData(final Image image) { try (final Connection connection = getConnection()) { storeCoreMetaData(connection, image); storeExternalUrls(connection, image); templateFactory.storeImageTemplates(connection, image); return new InsertUpdateResult<>(makeImage(image.getKey(), connection)); } catch (SQLException e) { getLogger().error("Error caught when executing SQL: storeImageMetaData", e); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, e.getMessage()); } } @Override public InsertUpdateResult createImageOutline(final ImageOutlineRequest request) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(CreateImageOutline)) { int i = 1; call.setInt(i++, request.getRepositoryKey().getId()); call.setString(i++, request.getImageName()); call.setString(i++, request.getImageDescription()); Utils.setNullableTimestamp(call, i++, request.getImageLastUpdated()); call.setBoolean(i++, ItemSyncSpec.Default.isDeprecated()); call.setBoolean(i++, ItemSyncSpec.Default.isHidden()); call.setBoolean(i++, ItemSyncSpec.Default.isStable()); call.setBoolean(i++, ItemSyncSpec.Default.isSynchronised()); call.setString(i++, ItemSyncSpec.Default.getVersionMask()); final int statusIndex = i; call.registerOutParameter(statusIndex, Types.VARCHAR); final ResultSet results = call.executeQuery(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(statusIndex)); if (status.isExists()) { return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "Image already exists"); } if (results.next()) { return new InsertUpdateResult<>(makeImage(makeImageKey(results), connection)); } return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "createImageOutline did not return anything."); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: createImageOutline", e); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, e.getMessage()); } } @Override public InsertUpdateResult createTagBranchOutline(final TagBranchOutlineRequest request) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(CreateTagBranchOutline)) { int i = 1; call.setInt(i++, request.getImageKey().getId()); call.setString(i, request.getBranchName()); final ResultSet results = call.executeQuery(); if (results.next()) { return new InsertUpdateResult<>(makeTagBranch(results, connection, request.getImageKey())); } return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "createTagBranchOutline did not return anything."); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: createTagBranchOutline", e); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, e.getMessage()); } } @Override public InsertUpdateResult removeImage(final Image image) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(DeleteImage)) { call.setInt(1, image.getKey().getId()); call.registerOutParameter(2, Types.VARCHAR); call.executeUpdate(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(2)); if (status.isNoChange()) { getLogger().warn("removeImage attempted to remove an image which did not exist in the database: {}", image); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "Unable to remove image " + image); } return new InsertUpdateResult<>(null); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: removeImage", e); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, e.getMessage()); } } @Override public Repository fetchRepository(final RepositoryKey repositoryKey) { try (final Connection connection = getConnection()) { return makeRepository(repositoryKey, connection); } catch (SQLException e) { getLogger().error("Error caught when executing SQL: fetchRepository", e); throw new RuntimeException("fetchRepository", e); } } @Override public InsertUpdateResult createRepositoryOutline(final RepositoryOutlineRequest request) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(CreateRepositoryOutline)) { int i = 1; call.setString( i++, request.getRepositoryName()); call.setTimestamp(i++, Timestamp.valueOf(LocalDateTime.now())); call.setBoolean( i++, ItemSyncSpec.Default.isDeprecated()); call.setBoolean( i++, ItemSyncSpec.Default.isHidden()); call.setBoolean( i++, ItemSyncSpec.Default.isStable()); call.setBoolean( i++, ItemSyncSpec.Default.isSynchronised()); call.setString( i++, ItemSyncSpec.Default.getVersionMask()); final int statusIndex = i; call.registerOutParameter(statusIndex, Types.VARCHAR); final ResultSet results = call.executeQuery(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(statusIndex)); if (status.isExists()) { return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "Repository already exists"); } if (results.next()) { return new InsertUpdateResult<>(makeRepository(makeRepositoryKey(results), connection)); } return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "createRepositoryOutline did not return anything."); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: createRepositoryOutline", e); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, e.getMessage()); } } @Override public List fetchAllRepositories() { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(GetRepositoryKeys)) { final ResultSet results = call.executeQuery(); final List repositoryKeys = new ArrayList<>(); while (results.next()) { repositoryKeys.add(makeRepositoryKey(results)); } return makeRepositories(repositoryKeys, connection); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: fetchAllRepositories", e); throw new RuntimeException("fetchAllRepositories", e); } } @Override public InsertUpdateResult storeRepository(Repository repository) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(StoreRepository)) { int i = 1; call.setInt(i++, repository.getKey().getId()); call.setBoolean(i++, repository.getSpec().isSynchronised()); call.setString(i++, repository.getSpec().getVersionMask()); final int statusIndex = i; call.registerOutParameter(statusIndex, Types.VARCHAR); call.executeUpdate(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(statusIndex)); if (status.isUpdated()) { return new InsertUpdateResult<>(makeRepository(repository.getKey(), connection)); } return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "Repository was not updated."); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: storeRepository", e); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, e.getMessage()); } } @Override public InsertUpdateResult removeRepository(final Repository repository) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(DeleteRepository)) { call.setInt(1, repository.getKey().getId()); call.registerOutParameter(2, Types.VARCHAR); call.executeUpdate(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(2)); if (status.isNoChange()) { getLogger().warn("Attempted to delete repository {} but resulted in no change", repository); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "Unable to remove repository " + repository.getName()); } return new InsertUpdateResult<>(null); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: removeRepository.", e); return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, e.getMessage()); } } private void storeCoreMetaData(final Connection connection, final Image image) throws SQLException { try (final CallableStatement call = connection.prepareCall(StoreCoreMetaData)) { int i = 1; call.setInt(i++, image.getKey().getId()); Utils.setNullableString(call, i++, image.getMetaData().getCategory()); Utils.setNullableString(call, i++, image.getMetaData().getBaseImage()); Utils.setNullableString(call, i++, image.getMetaData().getAppImagePath()); final int statusIndex = i; call.registerOutParameter(statusIndex, Types.VARCHAR); call.executeUpdate(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(statusIndex)); getLogger().info("storeCoreMetaData stored with result " + status); } } private void storeExternalUrls(final Connection connection, final Image image) throws SQLException { final List urls = image.getMetaData().getExternalUrls(); removeOrphans(connection, image.getKey(), urls, RemoveOrphanUrls); try (final CallableStatement call = connection.prepareCall(StoreExternalUrl)) { for (ExternalUrl url : urls) { int i = 1; call.setInt( i++, image.getKey().getId()); call.setInt( i++, url.getKey().getId()); call.setString(i++, url.getType().name()); call.setString(i++, url.getName()); call.setString(i, url.getAbsoluteUrl()); call.addBatch(); } call.executeBatch(); } } private void storeTagBranches(final Connection connection, final Image image) throws SQLException { final List tagBranches = image.getTagBranches(); removeOrphans(connection, image.getKey(), tagBranches, RemoveOrphanBranches); try (final CallableStatement call = connection.prepareCall(StoreTagBranch)) { for (TagBranch tagBranch : tagBranches) { int i = 1; call.setInt(i++, tagBranch.getKey().getImageKey().getId()); call.setInt(i++, tagBranch.getKey().getId()); call.setString(i++, tagBranch.getLatestTag().getVersion()); call.setTimestamp(i, Timestamp.valueOf(tagBranch.getLatestTag().getBuildDate())); call.addBatch(); } call.executeBatch(); } for (TagBranch tagBranch : tagBranches) { storeTagDigests(connection, tagBranch); } } private void removeOrphans(final Connection connection, final ImageKey imageKey, final List> possibleOrphans, final String sprocSpec) throws SQLException { final String ids = possibleOrphans.stream().map(o -> String.valueOf(o.getKey().getId())).collect(Collectors.joining(",")); try (final CallableStatement call = connection.prepareCall(sprocSpec)) { call.setInt( 1, imageKey.getId()); call.setString(2, ids); call.executeUpdate(); } } private void storeTagDigests(final Connection connection, final TagBranch tagBranch) throws SQLException { try (final CallableStatement call = connection.prepareCall(StoreTagDigest)) { for (TagDigest digest : tagBranch.getLatestTag().getDigests()) { int i = 1; call.setInt(i++, tagBranch.getKey().getId()); call.setLong(i++, digest.getSize()); call.setString(i++, digest.getDigest()); call.setString(i++, digest.getArchitecture()); call.setString(i, digest.getArchVariant()); call.addBatch(); } call.executeBatch(); } } private List makeRepositories(final List repositoryKeys, final Connection connection) throws SQLException { final List repositories = new ArrayList<>(); try (final CallableStatement call = connection.prepareCall(GetRepository)) { for (RepositoryKey key : repositoryKeys) { call.setInt(1, key.getId()); final Repository repository = makeOneRepository(connection, call); if (null != repository) { repositories.add(repository); } else { getLogger().warn("makeRepositories attempted to make repository for key {} but none exists. Skipping.", key); } } } return repositories; } private Image makeImage(final ImageKey imageKey, final Connection connection) throws SQLException { try (final CallableStatement call = connection.prepareCall(GetImage)) { call.setInt(1, imageKey.getId()); final Image image = makeOneImage(connection, call); if (null == image) { getLogger().info("No image with key {} found", imageKey); } return image; } } private List makeImages(final List imageKeys, final Connection connection) throws SQLException { final List images = new ArrayList<>(); try (final CallableStatement call = connection.prepareCall(GetImage)) { for (ImageKey key : imageKeys) { call.setInt(1, key.getId()); final Image image = makeOneImage(connection, call); if (null != image) { images.add(image); } else { getLogger().warn("makeImages attempted to make image for key {} but none exists. Skipping.", key); } } } return images; } private Image makeOneImage(final Connection connection, final CallableStatement call) throws SQLException { final ResultSet results = call.executeQuery(); if (results.next()) { final ImageKey imageKey = makeImageKey(results); final Image image = new Image(imageKey, makeSyncSpec(results), makeImageMetaData(connection, imageKey, results), makeCountData(results), results.getString("Description"), results.getTimestamp("LastUpdated").toLocalDateTime()); enrichImageWithTagBranches(image, connection); return image; } return null; } private ImageMetaData makeImageMetaData(final Connection connection, final ImageKey imageKey, final ResultSet mainImageResults) throws SQLException { return new ImageMetaData(makeCoreMeta(connection, imageKey, mainImageResults), makePullHistory(connection, imageKey), templateFactory.makeTemplateHolder(connection, imageKey)); } private ImageCoreMeta makeCoreMeta(final Connection connection, final ImageKey imageKey, final ResultSet mainImageResults) throws SQLException { final ImageCoreMeta coreMeta = new ImageCoreMeta(mainImageResults.getString("CoreMetaImagePath"), mainImageResults.getString("CoreMetaBaseImage"), mainImageResults.getString("CoreMetaCategory")); makeExternalUrls(connection, imageKey).forEach(coreMeta::addExternalUrl); return coreMeta; } private List makeExternalUrls(final Connection connection, final ImageKey imageKey) throws SQLException { final List externalUrls = new ArrayList<>(); try (final CallableStatement call = connection.prepareCall(GetExternalUrls)) { call.setInt(1, imageKey.getId()); final ResultSet results = call.executeQuery(); while (results.next()) { externalUrls.add(new ExternalUrl(new ExternalUrlKey(results.getInt("UrlId")), ExternalUrl.ExternalUrlType.valueOf(results.getString("UrlType")), results.getString("UrlName"), results.getString("UrlPath"))); } } return externalUrls; } private ImagePullHistory makePullHistory(final Connection connection, final ImageKey imageKey) throws SQLException { final ImagePullHistory pullHistory = new ImagePullHistory(); try (final CallableStatement call = connection.prepareCall(GetImageStats)) { call.setInt(1, imageKey.getId()); final ResultSet results = call.executeQuery(); while (results.next()) { final long imagePulls = results.getLong("ImagePulls"); if (!results.wasNull()) { final String timeGroup = results.getString("TimeGroup"); final String groupMode = results.getString("GroupMode"); final ImagePullStatistic statistic = new ImagePullStatistic(imagePulls, timeGroup, ImagePullStatistic.StatGroupMode.valueOf(groupMode)); final boolean added = pullHistory.addStatistic(statistic); if (!added) { getLogger().warn("Unable to add pull history {} to image {}", statistic, imageKey); } } } } return pullHistory; } private ImageKey makeImageKey(final ResultSet results) throws SQLException { return new ImageKey(results.getInt("ImageId"), results.getString("ImageName"), makeRepositoryKey(results)); } private Repository makeRepository(final RepositoryKey repositoryKey, final Connection connection) throws SQLException { try (final CallableStatement call = connection.prepareCall(GetRepository)) { call.setInt(1, repositoryKey.getId()); return makeOneRepository(connection, call); } } private Repository makeOneRepository(final Connection connection, final CallableStatement call) throws SQLException { final ResultSet results = call.executeQuery(); if (results.next()) { final Repository repository = new Repository(makeRepositoryKey(results), makeSyncSpec(results)); enrichRepositoryWithImages(repository, connection); return repository; } return null; } private void enrichRepositoryWithImages(final Repository repository, final Connection connection) throws SQLException { try (final CallableStatement call = connection.prepareCall(GetImageKeys)) { call.setInt(1, repository.getKey().getId()); final List repositoryImageKeys = new ArrayList<>(); final ResultSet results = call.executeQuery(); while (results.next()) { repositoryImageKeys.add(makeImageKey(results)); } makeImages(repositoryImageKeys, connection).forEach(repository::addImage); } } private RepositoryKey makeRepositoryKey(final ResultSet results) throws SQLException { return new RepositoryKey(results.getInt("RepositoryId"), results.getString("RepositoryName")); } private ItemSyncSpec makeSyncSpec(final ResultSet results) throws SQLException { return new ItemSyncSpec(results.getBoolean("Deprecated"), results.getBoolean("Hidden"), results.getBoolean("Stable"), results.getBoolean("SyncEnabled"), results.getString("VersionMask")); } private ImageCountData makeCountData(final ResultSet results) throws SQLException { return new ImageCountData(results.getLong("LatestPullCount"), results.getInt("LatestStarCount")); } private void enrichImageWithTagBranches(final Image image, final Connection connection) throws SQLException { try (final CallableStatement call = connection.prepareCall(GetTagBranches)) { call.setInt(1, image.getKey().getId()); final ResultSet results = call.executeQuery(); while (results.next()) { image.addTagBranch(makeTagBranch(results, connection, image.getKey())); } } } private TagBranch makeTagBranch(final ResultSet results, final Connection connection, final ImageKey imageKey) throws SQLException { final TagBranchKey tagBranchKey = makeTagBranchKey(results, imageKey); return new TagBranch(tagBranchKey, results.getString("BranchName"), results.getBoolean("BranchProtected"), makeTag(results, connection, tagBranchKey)); } private TagBranchKey makeTagBranchKey(ResultSet results, ImageKey imageKey) throws SQLException { return new TagBranchKey(results.getInt("BranchId"), imageKey); } private Tag makeTag(final ResultSet results, final Connection connection, final TagBranchKey tagBranchKey) throws SQLException { return new Tag(results.getString("TagVersion"), results.getTimestamp("TagBuildDate").toLocalDateTime(), makeTagDigests(connection, tagBranchKey)); } private Set makeTagDigests(final Connection connection, final TagBranchKey tagBranchKey) throws SQLException { final Set digests = new HashSet<>(); try (final CallableStatement call = connection.prepareCall(GetTagDigests)) { call.setInt(1, tagBranchKey.getId()); final ResultSet results = call.executeQuery(); while (results.next()) { digests.add(makeTagDigest(results)); } } return digests; } private TagDigest makeTagDigest(final ResultSet results) throws SQLException { return new TagDigest(results.getLong("DigestSize"), results.getString("DigestSha"), results.getString("DigestArch"), results.getString("DigestVariant")); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/DefaultScheduleDAO.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import io.linuxserver.fleet.core.db.DatabaseProvider; import io.linuxserver.fleet.v2.key.ScheduleKey; import io.linuxserver.fleet.v2.thread.schedule.AppSchedule; import io.linuxserver.fleet.v2.thread.schedule.ScheduleSpec; import io.linuxserver.fleet.v2.thread.schedule.TimeWithUnit; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashSet; import java.util.Set; public class DefaultScheduleDAO extends AbstractDAO implements ScheduleDAO { private static final String GetScheduleSpecs = "{CALL Schedule_GetSpecs()}"; public DefaultScheduleDAO(final DatabaseProvider databaseProvider) { super(databaseProvider); } @Override public Set fetchScheduleSpecs() { final Set specs = new HashSet<>(); try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(GetScheduleSpecs)) { final ResultSet results = call.executeQuery(); while (results.next()) { specs.add(makeOneScheduleSpec(results)); } } catch (ClassNotFoundException e) { getLogger().error("No class found for schedule. Ignoring this schedule.", e); } } catch (SQLException e) { getLogger().error("Error caught when executing SQL: fetchScheduleSpecs", e); throw new RuntimeException("fetchScheduleSpecs", e); } return specs; } @SuppressWarnings("unchecked") private ScheduleSpec makeOneScheduleSpec(final ResultSet results) throws SQLException, ClassNotFoundException { return ScheduleSpec.makeInitial(new ScheduleKey(results.getInt("ScheduleId")), results.getString("ScheduleName"), TimeWithUnit.valueOf(results.getString("ScheduleInterval")), TimeWithUnit.valueOf(results.getString("ScheduleDelayOffset")), (Class) Class.forName(results.getString("ScheduleClass"))); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/DefaultUserDAO.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import io.linuxserver.fleet.core.db.DatabaseProvider; import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.db.query.InsertUpdateStatus; import io.linuxserver.fleet.v2.key.UserKey; import io.linuxserver.fleet.v2.types.User; import io.linuxserver.fleet.v2.types.internal.UserOutlineRequest; import io.linuxserver.fleet.v2.web.AppRole; import java.sql.*; import java.util.ArrayList; import java.util.List; public class DefaultUserDAO extends AbstractDAO implements UserDAO { private static final String GetUser = "{CALL User_Get(?)}"; private static final String GetUserByName = "{CALL User_GetByName(?)}"; private static final String GetAllUsers = "{CALL User_GetAll()}"; private static final String CreateUser = "{CALL User_CreateOutline(?,?,?,?)}"; private static final String UpdateUser = "{CALL User_Save(?,?,?,?,?)}"; private static final String DeleteUser = "{CALL User_Delete(?)}"; public DefaultUserDAO(DatabaseProvider databaseProvider) { super(databaseProvider); } @Override public User fetchUser(final UserKey userKey) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(GetUser)) { call.setInt(1, userKey.getId()); final ResultSet results = call.executeQuery(); if (results.next()) { return makeOneUser(results); } } return null; } catch (SQLException e) { getLogger().error("fetchAllUsers unable to complete request", e); throw new RuntimeException("fetchAllUsers", e); } } @Override public User lookUpUser(final String username) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(GetUserByName)) { call.setString(1, username); final ResultSet results = call.executeQuery(); if (results.next()) { return makeOneUser(results); } } return null; } catch (SQLException e) { getLogger().error("fetchAllUsers unable to complete request", e); throw new RuntimeException("fetchAllUsers", e); } } @Override public InsertUpdateResult createUser(final UserOutlineRequest request) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(CreateUser)) { int i = 1; call.setString(i++, request.getUsername()); call.setString(i++, request.getPassword()); call.setString(i++, request.getRole().name()); final int statusIndex = i; call.registerOutParameter(statusIndex, Types.VARCHAR); final ResultSet results = call.executeQuery(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(statusIndex)); if (results.next() && status.isInserted()) { return new InsertUpdateResult<>(makeOneUser(results)); } if (status.isExists()) { return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "User already exists"); } } return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "Unknown error"); } catch (SQLException e) { getLogger().error("createUser unable to complete request", e); throw new RuntimeException("createUser", e); } } @Override public List fetchAllUsers() { final List users = new ArrayList<>(); try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(GetAllUsers)) { final ResultSet results = call.executeQuery(); while (results.next()) { users.add(makeOneUser(results)); } } } catch (SQLException e) { getLogger().error("fetchAllUsers unable to complete request", e); throw new RuntimeException("fetchAllUsers", e); } return users; } @Override public InsertUpdateResult removeUser(final User user) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(DeleteUser)) { call.setInt(1, user.getKey().getId()); call.executeUpdate(); } return new InsertUpdateResult<>(InsertUpdateStatus.OK, "OK"); } catch (SQLException e) { getLogger().error("updateUser unable to complete request", e); throw new RuntimeException("updateUser", e); } } @Override public InsertUpdateResult updateUser(final User updatedUser) { try (final Connection connection = getConnection()) { try (final CallableStatement call = connection.prepareCall(UpdateUser)) { int i = 1; call.setInt( i++, updatedUser.getKey().getId()); call.setString(i++, updatedUser.getUsername()); call.setString(i++, updatedUser.getPassword()); call.setString(i++, updatedUser.getRole().name()); final int statusIndex = i; call.registerOutParameter(statusIndex, Types.VARCHAR); final ResultSet results = call.executeQuery(); final DbUpdateStatus status = DbUpdateStatus.valueOf(call.getString(statusIndex)); if (results.next() && status.isUpdated()) { return new InsertUpdateResult<>(makeOneUser(results)); } if (status.isNoChange()) { return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "No user found"); } } return new InsertUpdateResult<>(InsertUpdateStatus.FAILED, "Unknown error"); } catch (SQLException e) { getLogger().error("updateUser unable to complete request", e); throw new RuntimeException("updateUser", e); } } private User makeOneUser(final ResultSet results) throws SQLException { return new User(new UserKey(results.getInt("UserId")), results.getString("Username"), results.getString("UserPassword"), results.getTimestamp("ModifiedTime").toLocalDateTime(), AppRole.valueOf(results.getString("UserRole"))); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/ImageDAO.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.Repository; import io.linuxserver.fleet.v2.types.TagBranch; import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest; import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest; import io.linuxserver.fleet.v2.types.internal.TagBranchOutlineRequest; import java.time.LocalDate; import java.util.List; public interface ImageDAO { Image fetchImage(final ImageKey imageKey); InsertUpdateResult storeImage(final Image image); InsertUpdateResult storeImageMetaData(final Image image); InsertUpdateResult createImageOutline(final ImageOutlineRequest request); InsertUpdateResult createTagBranchOutline(final TagBranchOutlineRequest request); InsertUpdateResult removeImage(final Image image); Repository fetchRepository(final RepositoryKey repositoryKey); InsertUpdateResult createRepositoryOutline(final RepositoryOutlineRequest request); List fetchAllRepositories(); InsertUpdateResult storeRepository(Repository repository); InsertUpdateResult removeRepository(final Repository repository); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/ImageTemplateFactory.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.docker.DockerCapability; import io.linuxserver.fleet.v2.types.meta.template.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; public class ImageTemplateFactory { private static final Logger LOGGER = LoggerFactory.getLogger(ImageTemplateFactory.class); private static final String ClearTemplateData = "{CALL Image_ClearTemplateData(?,?,?,?,?,?)}"; private static final String StoreTemplateBase = "{CALL Image_StoreTemplateBase(?,?,?,?,?,?)}"; private static final String StoreTemplatePort = "{CALL Image_StoreTemplatePort(?,?,?,?)}"; private static final String StoreTemplateVolume = "{CALL Image_StoreTemplateVolume(?,?,?,?)}"; private static final String StoreTemplateEnv = "{CALL Image_StoreTemplateEnv(?,?,?,?)}"; private static final String StoreTemplateDevice = "{CALL Image_StoreTemplateDevice(?,?,?)}"; private static final String StoreTemplateExtra = "{CALL Image_StoreTemplateExtra(?,?,?)}"; private static final String GetImageTemplateBase = "{CALL Image_GetTemplateBase(?)}"; private static final String GetImageTemplates = "{CALL Image_GetTemplates(?)}"; public final ImageTemplateHolder makeTemplateHolder(final Connection connection, final ImageKey imageKey) throws SQLException { String registryUrl = null; String restartPolicy = null; boolean hostNetworkingEnabled = false; boolean privilegedMode = false; try (final CallableStatement call = connection.prepareCall(GetImageTemplateBase)) { call.setInt(1, imageKey.getId()); final ResultSet results = call.executeQuery(); if (results.next()) { registryUrl = results.getString("RepositoryUrl"); restartPolicy = results.getString("RestartPolicy"); hostNetworkingEnabled = results.getBoolean("HostNetworkEnabled"); privilegedMode = results.getBoolean("PrivilegedMode"); } } final ImageTemplateHolder templateHolder = new ImageTemplateHolder(registryUrl, restartPolicy, hostNetworkingEnabled, privilegedMode); enrichHolderWithTemplates(connection, imageKey, templateHolder); return templateHolder; } public final void storeImageTemplates(final Connection connection, final Image image) throws SQLException { CallableStatement clearTemplatesCall = null; CallableStatement storePortCall = null; CallableStatement storeVolumeCall = null; CallableStatement storeEnvCall = null; CallableStatement storeDeviceCall = null; CallableStatement storeCapabilityCall = null; CallableStatement storeBaseCall = null; try { connection.setAutoCommit(false); clearTemplatesCall = connection.prepareCall(ClearTemplateData); storeBaseCall = connection.prepareCall(StoreTemplateBase); storePortCall = connection.prepareCall(StoreTemplatePort); storeVolumeCall = connection.prepareCall(StoreTemplateVolume); storeEnvCall = connection.prepareCall(StoreTemplateEnv); storeDeviceCall = connection.prepareCall(StoreTemplateDevice); storeCapabilityCall = connection.prepareCall(StoreTemplateExtra); clearTemplates( clearTemplatesCall, image); storeTemplateBase( storeBaseCall, image); storeTemplatePorts( storePortCall, image); storeTemplateVolumes( storeVolumeCall, image); storeTemplateEnv( storeEnvCall, image); storeTemplateDevices( storeDeviceCall, image); storeTemplateCapabilities(storeCapabilityCall, image); connection.commit(); } catch (SQLException e) { LOGGER.error("storeImageTemplates unable to complete transaction, rolling back", e); connection.rollback(); throw new SQLException(e); } finally { Utils.safeClose(clearTemplatesCall); Utils.safeClose(storePortCall); Utils.safeClose(storeVolumeCall); Utils.safeClose(storeEnvCall); Utils.safeClose(storeDeviceCall); Utils.safeClose(storeCapabilityCall); connection.setAutoCommit(true); } } private void enrichHolderWithTemplates(final Connection connection, final ImageKey imageKey, final ImageTemplateHolder templateHolder) throws SQLException { try (final CallableStatement call = connection.prepareCall(GetImageTemplates)) { call.setInt(1, imageKey.getId()); final ResultSet results = call.executeQuery(); while (results.next()) { final String itemType = results.getString("ItemType"); final String itemName = results.getString("ItemName"); final String itemDesc = results.getString("ItemDescription"); final String itemSec = results.getString("ItemSecondary"); switch (itemType) { case "Port": templateHolder.addPort(new PortTemplateItem(Integer.parseInt(itemName), itemDesc, PortTemplateItem.Protocol.fromName(itemSec))); break; case "Volume": templateHolder.addVolume(new VolumeTemplateItem(itemName, itemDesc, "1".equalsIgnoreCase(itemSec))); break; case "Env": templateHolder.addEnvironment(new EnvironmentTemplateItem(itemName, itemDesc, itemSec)); break; case "Device": templateHolder.addDevice(new DeviceTemplateItem(itemName, itemDesc)); break; case "Extra": templateHolder.addCapability(DockerCapability.valueOf(itemName)); break; default: LOGGER.warn("Found unknown template type " + itemType); } } } } private void clearTemplates(final CallableStatement clearTemplatesCall, final Image image) throws SQLException { final String volumes = joinStream(image.getMetaData().getTemplates().getVolumes(), VolumeTemplateItem::getVolume); final String ports = joinStream(image.getMetaData().getTemplates().getPorts(), (p) -> String.valueOf(p.getPort())); final String env = joinStream(image.getMetaData().getTemplates().getEnv(), EnvironmentTemplateItem::getEnv); final String caps = joinStream(image.getMetaData().getTemplates().getCapabilities(), DockerCapability::name); final String devices = joinStream(image.getMetaData().getTemplates().getDevices(), DeviceTemplateItem::getDevice); int i = 1; clearTemplatesCall.setInt( i++, image.getKey().getId()); clearTemplatesCall.setString(i++, volumes); clearTemplatesCall.setString(i++, ports); clearTemplatesCall.setString(i++, env); clearTemplatesCall.setString(i++, caps); clearTemplatesCall.setString(i, devices); clearTemplatesCall.executeUpdate(); } private String joinStream(final List list, final Function getter) { return list.stream().map(getter).collect(Collectors.joining(",")); } private void storeTemplateBase(final CallableStatement storeBaseCall, final Image image) throws SQLException { int i = 1; storeBaseCall.setInt(i++, image.getKey().getId()); storeBaseCall.setString(i++, image.getMetaData().getTemplates().getRegistryUrl()); storeBaseCall.setString(i++, image.getMetaData().getTemplates().getRestartPolicy()); storeBaseCall.setBoolean(i++, image.getMetaData().getTemplates().isHostNetworkingEnabled()); storeBaseCall.setBoolean(i++, image.getMetaData().getTemplates().isPrivilegedMode()); final int statusIndex = i; storeBaseCall.registerOutParameter(i, Types.VARCHAR); storeBaseCall.executeUpdate(); final DbUpdateStatus status = DbUpdateStatus.valueOf(storeBaseCall.getString(statusIndex)); LOGGER.info("storeTemplateBase update response=" + status); } private void storeTemplateCapabilities(final CallableStatement storeCapabilityCall, final Image image) throws SQLException { for (DockerCapability cap : image.getMetaData().getTemplates().getCapabilities()) { int i = 1; storeCapabilityCall.setInt( i++, image.getKey().getId()); storeCapabilityCall.setString(i++, cap.name()); storeCapabilityCall.setNull( i, Types.VARCHAR); storeCapabilityCall.addBatch(); } storeCapabilityCall.executeBatch(); } private void storeTemplateDevices(final CallableStatement storeDeviceCall, final Image image) throws SQLException { for (DeviceTemplateItem device : image.getMetaData().getTemplates().getDevices()) { int i = 1; storeDeviceCall.setInt( i++, image.getKey().getId()); storeDeviceCall.setString(i++, device.getDevice()); storeDeviceCall.setString(i, device.getDescription()); storeDeviceCall.addBatch(); } storeDeviceCall.executeBatch(); } private void storeTemplateEnv(final CallableStatement storeEnvCall, final Image image) throws SQLException { for (EnvironmentTemplateItem env : image.getMetaData().getTemplates().getEnv()) { int i = 1; storeEnvCall.setInt( i++, image.getKey().getId()); storeEnvCall.setString(i++, env.getEnv()); storeEnvCall.setString(i++, env.getDescription()); storeEnvCall.setString(i, env.getExampleValue()); storeEnvCall.addBatch(); } storeEnvCall.executeBatch(); } private void storeTemplateVolumes(final CallableStatement storeVolumeCall, final Image image) throws SQLException { for (VolumeTemplateItem volume : image.getMetaData().getTemplates().getVolumes()) { int i = 1; storeVolumeCall.setInt( i++, image.getKey().getId()); storeVolumeCall.setString( i++, volume.getVolume()); storeVolumeCall.setString( i++, volume.getDescription()); storeVolumeCall.setBoolean(i, volume.isReadonly()); storeVolumeCall.addBatch(); } storeVolumeCall.executeBatch(); } private void storeTemplatePorts(final CallableStatement storePortCall, final Image image) throws SQLException { for (PortTemplateItem port : image.getMetaData().getTemplates().getPorts()) { int i = 1; storePortCall.setInt( i++, image.getKey().getId()); storePortCall.setInt( i++, port.getPort()); storePortCall.setString(i++, port.getDescription()); storePortCall.setString(i, port.getProtocol()); storePortCall.addBatch(); } storePortCall.executeBatch(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/ScheduleDAO.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import io.linuxserver.fleet.v2.thread.schedule.ScheduleSpec; import java.util.Set; public interface ScheduleDAO { Set fetchScheduleSpecs(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/UserDAO.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.v2.key.UserKey; import io.linuxserver.fleet.v2.types.User; import io.linuxserver.fleet.v2.types.internal.UserOutlineRequest; import java.util.List; public interface UserDAO { User fetchUser(UserKey userKey); User lookUpUser(String username); InsertUpdateResult createUser(UserOutlineRequest request); List fetchAllUsers(); InsertUpdateResult removeUser(User user); InsertUpdateResult updateUser(User updatedUser); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/db/Utils.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.db; import java.sql.CallableStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.sql.Types; import java.time.LocalDateTime; class Utils { static void setNullableInt(CallableStatement call, int position, Integer value) throws SQLException { if (null == value) call.setNull(position, Types.INTEGER); else call.setInt(position, value); } static void setNullableLong(CallableStatement call, int position, Long value) throws SQLException { if (null == value) call.setNull(position, Types.BIGINT); else call.setLong(position, value); } static void setNullableString(CallableStatement call, int position, String value) throws SQLException { if (null == value) call.setNull(position, Types.VARCHAR); else call.setString(position, value); } static void setNullableTimestamp(CallableStatement call, int position, LocalDateTime localDateTime) throws SQLException { if (null == localDateTime) { call.setNull(position, Types.TIMESTAMP); } else { call.setTimestamp(position, Timestamp.valueOf(localDateTime)); } } static void safeClose(CallableStatement call) { try { if (null != call) call.close(); } catch (SQLException e) { throw new RuntimeException("Unable to close call", e); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/file/FileManager.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.file; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.service.AbstractAppService; import io.linuxserver.fleet.v2.types.FilePathDetails; import io.linuxserver.fleet.v2.types.internal.ImageAppLogo; import java.io.*; import java.nio.file.Path; import java.nio.file.Paths; public class FileManager extends AbstractAppService { private static final String imageDirName = "images"; private final String publicSafeImagesDir; private final Path staticImagesDir; public FileManager(final FleetAppController controller) { super(controller); staticImagesDir = Paths.get(getProperties().getStaticFilesPath().toString(), imageDirName); publicSafeImagesDir = "/" + imageDirName; makeImageUploadDir(); } public final FilePathDetails saveImageLogo(final ImageAppLogo logo) { if (logo.getMimeType().startsWith("image/")) { try { final FilePathDetails filePathDetails = makeFilePathDetails(logo); final File logoFile = new File(filePathDetails.getFullAbsolutePathWithFileName()); if (logoFile.exists()) { final boolean deleted = logoFile.delete(); if (!deleted) { getLogger().warn("Unable to delete file: " + logoFile); return null; } } boolean created = logoFile.createNewFile(); if (created) { writeDataToFile(logo, logoFile); return filePathDetails; } else { getLogger().warn("Unable to delete file: " + logoFile); return null; } } catch (IOException e) { getLogger().error("Unable to create logo file.", e); throw new RuntimeException(e); } } else { throw new IllegalArgumentException("Disallowed mimeType for file: " + logo.getMimeType()); } } private FilePathDetails makeFilePathDetails(final ImageAppLogo logo) { return new FilePathDetails(makePathSafeFileName(logo.getImageKey()) + logo.getFileExtension(), staticImagesDir.toString(), publicSafeImagesDir); } private String makePathSafeFileName(final ImageKey key) { return key.getAsRepositoryAndImageName().replace("/", "_"); } private void writeDataToFile(final ImageAppLogo logo, final File logoFile) throws IOException { try (final InputStream initialStream = logo.getRawDataStream(); final OutputStream out = new FileOutputStream(logoFile)) { byte[] buffer = new byte[8 * 1024]; int bytesRead; while ((bytesRead = initialStream.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } } private void makeImageUploadDir() { final File imageDir = new File(staticImagesDir.toString()); if (!imageDir.exists()) { getLogger().info("Creating new image directory for uploaded logos"); final boolean created = imageDir.mkdir(); if (!created) { throw new RuntimeException("Unable to create uploaded file dir. Check permissions"); } } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/AbstractDatabaseKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; public abstract class AbstractDatabaseKey implements Key { private final Integer id; public AbstractDatabaseKey(final Integer id) { this.id = id; } @Override public final Integer getId() { return id; } @Override public boolean equals(Object o) { if (!(o instanceof Key)) { return false; } if (null == id) { return ((Key) o).getId() == null; } return ((Key) o).getId().equals(id); } @Override public int hashCode() { if (null == id) { return -1; } return id.hashCode(); } @Override public String toString() { return null == id ? "" : String.valueOf(id); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/AbstractHasKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; import io.linuxserver.fleet.v2.Utils; public abstract class AbstractHasKey implements HasKey { private final KEY key; public AbstractHasKey(final KEY key) { this.key = Utils.ensureNotNull(key); } @Override public final KEY getKey() { return key; } @Override public int hashCode() { return key.hashCode(); } @Override public boolean equals(Object o) { if (!(o instanceof HasKey)) { return false; } return key.equals(((HasKey) o).getKey()); } @Override public String toString() { return key.toString(); } @Override public int compareTo(HasKey o) { if (null == o) { return -1; } final Integer otherId = o.getKey().getId(); final Integer thisId = getKey().getId(); if (null == otherId && null == thisId) { return 0; } if (null == otherId) { return -1; } else { return o.getKey().getId().compareTo(getKey().getId()); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/AbstractLookupKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; public abstract class AbstractLookupKey> implements Key { private final String query; public AbstractLookupKey(final String query) { this.query = query; } public final String getQuery() { return query; } @Override public final Integer getId() { return null; } @Override public String toString() { return query; } public abstract boolean isLookupKeyFor(final TYPE type); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/AlertKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; import io.linuxserver.fleet.v2.types.AppAlert; public class AlertKey extends AbstractLookupKey { public AlertKey(String query) { super(query); } @Override public boolean isLookupKeyFor(final AppAlert appAlert) { throw new RuntimeException("Operation not supported"); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/HasKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; public interface HasKey extends Comparable> { KEY getKey(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/ImageKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; public class ImageKey extends AbstractDatabaseKey { private static final String KeyPattern = "^\\d+:\\d+:[^/]+/[^/]+$"; private final RepositoryKey repositoryKey; private final String name; @Deprecated public ImageKey(final String name, final RepositoryKey repositoryKey) { this(null, name, repositoryKey); } public ImageKey(final Integer id, final String name, final RepositoryKey repositoryKey) { super(id); this.repositoryKey = repositoryKey; this.name = name; } public static ImageKey parse(final String keyAsString) { if (keyAsString.matches(KeyPattern)) { final String[] keyParts = keyAsString.split(":"); final String[] names = keyParts[2].split("/"); final int repositoryId = Integer.parseInt(keyParts[0]); final int imageId = Integer.parseInt(keyParts[1]); final String repositoryName = names[0]; final String imageName = names[1]; return new ImageKey(imageId, imageName, new RepositoryKey(repositoryId, repositoryName)); } else { throw new IllegalArgumentException("Key pattern is malformed"); } } public final ImageLookupKey getAsLookupKey() { return new ImageLookupKey(getRepositoryKey().getName() + "/" + getName()); } public final String getAsRepositoryAndImageName() { return getAsLookupKey().toString(); } public final RepositoryKey getRepositoryKey() { return repositoryKey; } public final String getName() { return name; } @Override public String toString() { return repositoryKey.getId() + ":" + super.toString() + ":" + repositoryKey.getName() + "/" + name; } @Override public boolean equals(Object o) { return super.equals(o) && ((ImageKey) o).name.equals(name); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/ImageLookupKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; import io.linuxserver.fleet.v2.types.Image; public class ImageLookupKey extends AbstractLookupKey { private static final String KeyPattern = "^[^/]+/[^/]+$"; private final String lookupRepositoryName; private final String lookupImageName; public ImageLookupKey(final String query) { super(query); if (query.matches(KeyPattern)) { final String[] names = query.split("/"); lookupRepositoryName = names[0]; lookupImageName = names[1]; } else { throw new IllegalArgumentException("Malformed lookup query for ImageLookupKey"); } } @Override public final boolean isLookupKeyFor(final Image image) { if (null == image) { return false; } return image.getRepositoryName().equals(lookupRepositoryName) && image.getName().equals(lookupImageName); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/Key.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; import javax.naming.OperationNotSupportedException; public interface Key { Integer getId(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/RepositoryKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; public class RepositoryKey extends AbstractDatabaseKey { private static final String KeyPattern = "^\\d+:[^/]++$"; private final String name; public static RepositoryKey parse(final String keyAsString) { if (keyAsString.matches(KeyPattern)) { final String[] keyParts = keyAsString.split(":"); final int repositoryId = Integer.parseInt(keyParts[0]); final String repositoryName = keyParts[1]; return new RepositoryKey(repositoryId, repositoryName); } else { throw new IllegalArgumentException("Key pattern is malformed"); } } public RepositoryKey(final Integer id, final String name) { super(id); this.name = name; } public RepositoryKey cloneWithId(int id) { return new RepositoryKey(id, name); } public final String getName() { return name; } @Override public String toString() { return super.toString() + ":" + name; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/ScheduleKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; public class ScheduleKey extends AbstractDatabaseKey { public ScheduleKey(Integer id) { super(id); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/TagBranchKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; public class TagBranchKey extends AbstractDatabaseKey { private final ImageKey imageKey; public TagBranchKey(final Integer id, final ImageKey imageKey) { super(id); this.imageKey = imageKey; } public final ImageKey getImageKey() { return imageKey; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/key/UserKey.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.key; public class UserKey extends AbstractDatabaseKey { public UserKey() { this(null); } public UserKey(Integer id) { super(id); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/service/AbstractAppService.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.service; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.core.config.AppProperties; import io.linuxserver.fleet.v2.LoggerOwner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AbstractAppService implements LoggerOwner { private final Logger logger; private final FleetAppController controller; public AbstractAppService(FleetAppController controller) { this.controller = controller; this.logger = LoggerFactory.getLogger(getClass()); } public final FleetAppController getController() { return controller; } public final AppProperties getProperties() { return getController().getAppProperties(); } public final Logger getLogger() { return logger; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/service/ImageService.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.service; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.dockerhub.util.DockerTagFinder; import io.linuxserver.fleet.v2.cache.RepositoryCache; import io.linuxserver.fleet.v2.db.ImageDAO; import io.linuxserver.fleet.v2.file.FileManager; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.ImageLookupKey; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.service.util.TemplateMerger; import io.linuxserver.fleet.v2.types.*; import io.linuxserver.fleet.v2.types.docker.DockerImage; import io.linuxserver.fleet.v2.types.docker.DockerTag; import io.linuxserver.fleet.v2.types.internal.*; import io.linuxserver.fleet.v2.types.meta.ImageCoreMeta; import io.linuxserver.fleet.v2.types.meta.ImageMetaData; import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; public class ImageService extends AbstractAppService { private static final Logger LOGGER = LoggerFactory.getLogger(ImageService.class); private final ImageDAO imageDAO; private final FileManager fileManager; private final RepositoryCache repositoryCache; private final TemplateMerger templateMerger; public ImageService(final FleetAppController controller, final ImageDAO imageDAO) { super(controller); this.imageDAO = imageDAO; this.fileManager = controller.getFileManager(); this.repositoryCache = new RepositoryCache(); this.templateMerger = new TemplateMerger(); reloadCache(); } public final void reloadCache() { final List allItems = imageDAO.fetchAllRepositories(); repositoryCache.clear(); repositoryCache.addAllItems(allItems); } public final Image updateImageSpec(final ImageKey imageKey, final ItemSyncSpec updatedSpec) { final Image cachedImage = getImage(imageKey); if (null == cachedImage) { throw new IllegalArgumentException("Image key " + imageKey + " did not match known image"); } final Image updated = cachedImage.cloneWithSyncSpec(updatedSpec); return storeImage(updated); } public final Repository updateRepositorySpec(final RepositoryKey repositoryKey, final ItemSyncSpec updatedSpec) { final Repository cachedRepository = getRepository(repositoryKey); if (null == cachedRepository) { throw new IllegalArgumentException("Repository key " + repositoryKey + " did not match known repository"); } final Repository updatedRepository = cachedRepository.cloneWithSyncSpec(updatedSpec); final InsertUpdateResult result = imageDAO.storeRepository(updatedRepository); if (result.isError()) { LOGGER.error("Unable to store repository {}. Update returned error: {}", repositoryKey, result.getStatusMessage()); throw new RuntimeException("Failed to store repository: " + result.getStatusMessage()); } final Repository storedRepository = result.getResult(); repositoryCache.addItem(storedRepository); return storedRepository; } public Repository createRepositoryOutline(final RepositoryOutlineRequest repositoryOutlineRequest) { final InsertUpdateResult result = imageDAO.createRepositoryOutline(repositoryOutlineRequest); if (result.isError()) { LOGGER.error("Unable to create repository outline {}, reason: {}", repositoryOutlineRequest, result.getStatusMessage()); throw new RuntimeException("Unable to create repository outline"); } final Repository repositoryOutline = result.getResult(); LOGGER.info("Successfully created outline for repository"); repositoryCache.addItem(repositoryOutline); return repositoryOutline; } public final Image createImageOutline(final ImageOutlineRequest request) { final InsertUpdateResult result = imageDAO.createImageOutline(request); if (result.isError()) { LOGGER.error("Unable to create image outline {}, reason: {}", request, result.getStatusMessage()); throw new RuntimeException("Unable to create outline"); } final Image imageOutline = result.getResult(); LOGGER.info("Successfully created outline for image {}", imageOutline); updateCache(imageOutline); return imageOutline; } public final void removeImage(final ImageKey imageKey) { final Image cachedImage = findImage(imageKey); final InsertUpdateResult removalResult = imageDAO.removeImage(cachedImage); if (removalResult.isError()) { throw new RuntimeException("Unable to remove persisted image: " + removalResult.getStatusMessage()); } repositoryCache.findItem(cachedImage.getRepositoryKey()).removeImage(cachedImage); } public final void removeRepository(final RepositoryKey repositoryKey) { if (repositoryCache.isItemCached(repositoryKey)) { final Repository cached = repositoryCache.findItem(repositoryKey); final InsertUpdateResult removalResult = imageDAO.removeRepository(cached); if (removalResult.isError()) { throw new RuntimeException("Unable to remove repository " + repositoryKey); } repositoryCache.removeItem(cached.getKey()); } else { throw new IllegalArgumentException("Unable to find cached repository " + repositoryKey); } } public final Image storeImage(final Image image) { return storeImage(image, imageDAO::storeImage); } public final Image storeImageTemplateMetaData(final Image image) { return storeImage(image, imageDAO::storeImageMetaData); } public final Image getImage(final ImageKey imageKey) { return repositoryCache.findImage(imageKey); } public final Image lookupImage(final ImageLookupKey imageLookupKey) { return repositoryCache.lookupImage(imageLookupKey); } public final Repository getRepository(final RepositoryKey repositoryKey) { return repositoryCache.findItem(repositoryKey); } public final Repository getFirstRepository() { return getAllShownRepositories().stream().findFirst().orElse(null); } public final List getAllRepositories() { return new ArrayList<>(repositoryCache.getAllItems()); } public final List getAllShownRepositories() { return getAllRepositories().stream().filter(r -> !r.isHidden()).collect(Collectors.toList()); } public Image applyImageUpstreamUpdate(final ImageKey imageKey, final DockerImage latestImage) { final Image cachedImage = findImage(imageKey); final Image cloned = cachedImage.cloneForUpdate(latestImage.getPullCount(), latestImage.getStarCount(), latestImage.getDescription(), latestImage.getBuildDate()); for (TagBranch branch : cloned.getTagBranches()) { final DockerTag matchingTag = DockerTagFinder.findVersionedTagMatchingBranch(latestImage.getTags(), branch.getBranchName()); if (null == matchingTag) { LOGGER.warn("Unable to find tag for branch {} in image {}. Will not update tags.", branch.getBranchName(), cloned.getFullName()); } else { branch.updateLatestTag(new Tag(matchingTag.getName(), matchingTag.getBuildDate(), matchingTag.getDigests().stream() .filter(Objects::nonNull) .map(d -> new TagDigest(d.getSize(), d.getDigest(), d.getArchitecture(), d.getArchVariant())).collect(Collectors.toSet()))); } } return storeImage(cloned); } public void trackBranchOnImage(final ImageKey imageKey, final String branchName) { final Image image = findImage(imageKey); if (image.findTagBranchByName(branchName) != null) { throw new IllegalArgumentException("Image is already tracking branch " + branchName); } final InsertUpdateResult outlineResult = imageDAO.createTagBranchOutline(new TagBranchOutlineRequest(imageKey, branchName)); if (outlineResult.isError()) { throw new RuntimeException(outlineResult.getStatusMessage()); } final Image updatableClone = image.cloneForUpdate(); updatableClone.addTagBranch(outlineResult.getResult()); storeImage(updatableClone); } public void removeTrackedBranch(final ImageKey imageKey, final String branchName) { final Image image = findImage(imageKey); final TagBranch branch = image.findTagBranchByName(branchName); if (branch == null) { throw new IllegalArgumentException("Could not find " + branchName); } final Image updatableClone = image.cloneForUpdate(); updatableClone.removeTagBranch(branch); storeImage(updatableClone); } public void updateImageGeneralInfo(final ImageKey imageKey, final ImageGeneralInfoUpdateRequest generalInfoUpdateRequest) { final Image image = findImage(imageKey); final ImageMetaData metaData = image.getMetaData(); String appLogoPath = metaData.getAppImagePath(); if (null != generalInfoUpdateRequest.getImageAppLogo()) { final FilePathDetails filePathDetails = fileManager.saveImageLogo(generalInfoUpdateRequest.getImageAppLogo()); if (null != filePathDetails) { appLogoPath = filePathDetails.getPublicSafePathWithFileName(); } } final ImageCoreMeta coreMeta = metaData.getCoreMeta().cloneWithBaseData(appLogoPath, generalInfoUpdateRequest.getBaseImage(), generalInfoUpdateRequest.getCategory()); final Image cloned = image.cloneWithMetaData(metaData.cloneWithCoreMeta(coreMeta)); storeImageTemplateMetaData(cloned); } public void updateImageExternalUrls(final ImageKey imageKey, final ImageUrlsUpdateRequest imageUrlsUpdateFields) { final Image image = findImage(imageKey); final ImageMetaData metaData = image.getMetaData(); final ImageCoreMeta coreMeta = metaData.getCoreMeta().cloneWithExternalUrls(imageUrlsUpdateFields.getExternalUrls()); final Image cloned = image.cloneWithMetaData(metaData.cloneWithCoreMeta(coreMeta)); storeImageTemplateMetaData(cloned); } public void updateImageTemplate(final ImageKey imageKey, final ImageTemplateRequest imageTemplateUpdateFields) { final Image image = findImage(imageKey); final Image cloned = templateMerger.mergeTemplateRequestIntoImage(image, imageTemplateUpdateFields); LOGGER.info("{} merged with new template", cloned); storeImageTemplateMetaData(cloned); } private synchronized Image storeImage(final Image image, final ImageStorage storageFunction) { final InsertUpdateResult result = storageFunction.store(image); if (result.isError()) { LOGGER.error("Unable to store image {}. Update returned error: {}", image, result.getStatusMessage()); throw new RuntimeException("Failed to store image: " + result.getStatusMessage()); } final Image storedImage = result.getResult(); updateCache(storedImage); return storedImage; } private Image findImage(ImageKey imageKey) { final Image image = repositoryCache.findImage(imageKey); if (null == image) { throw new IllegalArgumentException("Could not find image with key " + imageKey); } return image; } private void updateCache(final Image storedImage) { final Repository imageParentRepository = repositoryCache.findItem(storedImage.getRepositoryKey()); if (null != imageParentRepository) { imageParentRepository.addImage(storedImage); } else { LOGGER.warn("Could not find repository for image {}", storedImage); } } @FunctionalInterface interface ImageStorage { InsertUpdateResult store(final Image image); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/service/ScheduleService.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.service; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.cache.BasicItemCache; import io.linuxserver.fleet.v2.db.ScheduleDAO; import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.ScheduleKey; import io.linuxserver.fleet.v2.thread.schedule.AppSchedule; import io.linuxserver.fleet.v2.thread.schedule.ScheduleSpec; import io.linuxserver.fleet.v2.thread.schedule.TimeWithUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Constructor; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.stream.Collectors; public class ScheduleService extends AbstractAppService { private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleService.class); private final BasicItemCache scheduleCache; private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); private final ScheduleDAO scheduleDAO; public ScheduleService(final FleetAppController controller, final ScheduleDAO scheduleDAO) { super(controller); this.scheduleCache = new BasicItemCache<>(); this.scheduleDAO = scheduleDAO; } public final void initialiseSchedules() { final Set specs = scheduleDAO.fetchScheduleSpecs(); for (ScheduleSpec spec : specs) { try { final AppSchedule schedule = loadSchedule(spec); LOGGER.info("Schedule loaded: {}", schedule); loadOneSchedule(schedule); } catch (Exception e) { LOGGER.error("Unable to load schedule", e); } } } public final AppSchedule forceRun(final ScheduleKey scheduleKey) { if (scheduleCache.isItemCached(scheduleKey)) { final ScheduleWrapper wrapper = scheduleCache.findItem(scheduleKey); LOGGER.info("Cancelling current run of schedule {}", wrapper.getName()); final boolean stopped = wrapper.getFuture().cancel(false); if (stopped) { LOGGER.info("Triggering re-run of schedule {}", wrapper.getName()); loadOneScheduleImmediately(wrapper.getSchedule()); return wrapper.getSchedule(); } throw new RuntimeException("Schedule was already running and could not be stopped. Try again later."); } else { LOGGER.warn("Did not find cached schedule with key {}", scheduleKey); throw new IllegalArgumentException("No schedule found with key " + scheduleKey); } } public final List getLoadedSchedules() { return scheduleCache.getAllItems().stream().map(ScheduleWrapper::getSchedule).collect(Collectors.toList()); } private AppSchedule loadSchedule(final ScheduleSpec spec) { try { final Constructor scheduleConstructor = spec.getScheduleClass().getDeclaredConstructor(ScheduleSpec.class, FleetAppController.class); return scheduleConstructor.newInstance(spec, getController()); } catch (Exception e) { LOGGER.error("Unable to instantiate schedule for class {}", spec.getScheduleClass(), e); throw new RuntimeException(e); } } private void loadOneSchedule(final AppSchedule schedule) { loadInternal(schedule, schedule.getDelay()); } private void loadOneScheduleImmediately(final AppSchedule schedule) { loadInternal(schedule, TimeWithUnit.Zero); } private void loadInternal(final AppSchedule schedule, final TimeWithUnit delay) { final TimeWithUnit scheduleIntervalUnit = schedule.getInterval().convertToLowestUnit(delay); final TimeWithUnit scheduleDelayUnit = delay.convertToLowestUnit(schedule.getInterval()); LOGGER.info("Scheduling with calculated interval/delay: {}/{}", scheduleIntervalUnit, scheduleDelayUnit); final ScheduledFuture future = executorService.scheduleAtFixedRate(schedule, scheduleDelayUnit.getTimeDuration(), scheduleIntervalUnit.getTimeDuration(), scheduleIntervalUnit.getTimeUnit()); scheduleCache.addItem(new ScheduleWrapper(schedule, future)); } public static class ScheduleWrapper extends AbstractHasKey { private final ScheduledFuture future; private final AppSchedule schedule; public ScheduleWrapper(final AppSchedule schedule, final ScheduledFuture future) { super(schedule.getKey()); this.future = future; this.schedule = schedule; } public final ScheduledFuture getFuture() { return future; } public final AppSchedule getSchedule() { return schedule; } public final String getName() { return getSchedule().getName(); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/service/SynchronisationService.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.service; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.client.docker.queue.DockerApiDelegate; import io.linuxserver.fleet.v2.client.docker.queue.DockerApiTaskConsumer; import io.linuxserver.fleet.v2.client.docker.queue.DockerImageUpdateRequest; import io.linuxserver.fleet.v2.client.docker.queue.TaskQueue; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.ImageLookupKey; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.Repository; import io.linuxserver.fleet.v2.types.docker.DockerImage; import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest; import java.util.List; import java.util.stream.Collectors; public class SynchronisationService extends AbstractAppService { private final TaskQueue syncQueue; private final DockerApiTaskConsumer taskConsumer; public SynchronisationService(FleetAppController controller) { super(controller); syncQueue = new TaskQueue<>(); taskConsumer = new DockerApiTaskConsumer(this); taskConsumer.start(); } public final void synchroniseUpstreamRepository(final Repository repository) { if (repository.isSyncEnabled()) { getLogger().info("synchroniseUpstreamRepository checking {} for new images since last sync", repository); final List apiImages = getController().getConfiguredDockerDelegate().getImagesForRepository(repository.getKey()); for (DockerImage apiImage : apiImages) { final Image cachedImage = getController().getImageService() .lookupImage(new ImageLookupKey(apiImage.getRepository() + "/" + apiImage.getName())); if (null == cachedImage) { getLogger().info("Found image from API which is not currently cached. Will add to system: {}", apiImage); final ImageOutlineRequest outlineRequest = new ImageOutlineRequest(repository.getKey(), apiImage.getName(), apiImage.getDescription(), apiImage.getBuildDate()); final Image imageOutline = getController().getImageService().createImageOutline(outlineRequest); synchroniseImage(imageOutline.getKey()); } } } else { getLogger().info("Will not check upstream repository {} as synchronisation is disabled", repository); } } public final void synchroniseCachedRepository(final Repository repository) { if (repository.isSyncEnabled()) { for (Image image : repository.getImages()) { if (image.isSyncEnabled()) { boolean submitted = synchroniseImage(image.getKey()); if (!submitted) { getLogger().warn("Unable to place sync request for image {} on queue", image.getKey()); } } else { getLogger().info("Ignoring sync request for {} as it has synchronisation disabled.", image); } } } else { getLogger().info("Will not synchronise images in {} as it has synchronisation disabled", repository); } } public final boolean synchroniseImage(final ImageKey imageKey) { return syncQueue.submitTask(new DockerImageUpdateRequest(imageKey)); } public final TaskQueue getSyncQueue() { return syncQueue; } public final DockerApiDelegate getConfiguredDockerDelegate() { return getController().getConfiguredDockerDelegate(); } public final boolean isConsumerRunning() { return taskConsumer.isThreadRunning(); } public final boolean isSyncQueueEmpty() { return getSyncQueue().isEmpty(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/service/UserService.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.service; import io.linuxserver.fleet.auth.AuthenticationDelegate; import io.linuxserver.fleet.auth.AuthenticationResult; import io.linuxserver.fleet.auth.DefaultAuthenticationDelegate; import io.linuxserver.fleet.auth.authenticator.DefaultUserAuthenticator; import io.linuxserver.fleet.auth.security.PBKDF2PasswordEncoder; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.db.query.InsertUpdateResult; import io.linuxserver.fleet.v2.db.UserDAO; import io.linuxserver.fleet.v2.key.UserKey; import io.linuxserver.fleet.v2.types.User; import io.linuxserver.fleet.v2.types.internal.UserOutlineRequest; import java.util.List; public class UserService extends AbstractAppService { private final UserDAO userDAO; private final AuthenticationDelegate authDelegate; public UserService(final FleetAppController controller, final UserDAO userDAO) { super(controller); this.userDAO = userDAO; this.authDelegate = new DefaultAuthenticationDelegate(new DefaultUserAuthenticator(this, new PBKDF2PasswordEncoder(getProperties().getAppSecret()))); createInitialAdminUser(); } public final AuthenticationResult authenticateCredentials(final String username, final String password) { return authDelegate.authenticate(username, password); } public final User lookUpUser(final String username) { return userDAO.lookUpUser(username); } public final User fetchUser(final UserKey userKey) { return userDAO.fetchUser(userKey); } public final List fetchAllUsers() { return userDAO.fetchAllUsers(); } public final void removeUser(final User user) { userDAO.removeUser(user); } public final User updateUserPassword(final User user, final String password) { final User updatedUser = user.cloneWithPassword(authDelegate.encodePassword(password)); final InsertUpdateResult result = userDAO.updateUser(updatedUser); if (result.isError()) { getLogger().error("Unable to update user: {}", result.getStatusMessage()); throw new RuntimeException("Unable to update user: " + result.getStatusMessage()); } return result.getResult(); } public final User createUserAndHashPassword(final UserOutlineRequest userOutlineRequest) { return createUser(userOutlineRequest.cloneWithPassword(authDelegate.encodePassword(userOutlineRequest.getPassword()))); } public final User createUser(final UserOutlineRequest userOutlineRequest) { final InsertUpdateResult result = userDAO.createUser(userOutlineRequest); if (result.isError()) { getLogger().error("Unable to create new user: {}", result.getStatusMessage()); throw new RuntimeException("Unable to create new user: " + result.getStatusMessage()); } return result.getResult(); } private void createInitialAdminUser() { if (fetchAllUsers().isEmpty()) { getLogger().info("There are no users! Creating initial user with default credentials"); createUserAndHashPassword(UserOutlineRequest.InitialFirstLoadUser); getLogger().warn("!!!!!!!!"); getLogger().warn("DEFAULT USER CREATED. CHANGE THE PASSWORD OR CREATE A NEW USER!"); getLogger().warn("!!!!!!!!"); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/service/util/TemplateMerger.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.service.util; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.docker.DockerCapability; import io.linuxserver.fleet.v2.types.internal.ImageTemplateRequest; import io.linuxserver.fleet.v2.types.meta.template.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TemplateMerger { private static final Logger LOGGER = LoggerFactory.getLogger(TemplateMerger.class); public final Image mergeTemplateRequestIntoImage(final Image image, final ImageTemplateRequest templateRequest) { final ImageTemplateHolder templateHolder = makeTemplateHolder(templateRequest); addMisc( templateRequest, templateHolder); addPorts( templateRequest, templateHolder); addVolumes( templateRequest, templateHolder); addEnvironment(templateRequest, templateHolder); addDevices( templateRequest, templateHolder); return image.cloneWithMetaData(image.getMetaData().cloneWithTemplate(templateHolder)); } private ImageTemplateHolder makeTemplateHolder(final ImageTemplateRequest templateRequest) { return new ImageTemplateHolder(templateRequest.getRegistryUrl(), templateRequest.getRestartPolicy(), templateRequest.isHostNetworkEnabled(), templateRequest.isPrivilegedMode()); } private void addPorts(final ImageTemplateRequest request, final ImageTemplateHolder holder) { for (ImageTemplateRequest.TemplateItem port : request.getPorts()) { holder.addPort(new PortTemplateItem(Integer.parseInt(port.getName()), port.getDescription(), PortTemplateItem.Protocol.fromName(port.getSecondaryField()))); } } private void addVolumes(final ImageTemplateRequest request, final ImageTemplateHolder holder) { for (ImageTemplateRequest.TemplateItem volume : request.getVolumes()) { holder.addVolume(new VolumeTemplateItem(volume.getName(), volume.getDescription(), volume.getSecondaryField())); } } private void addEnvironment(final ImageTemplateRequest request, final ImageTemplateHolder holder) { for (ImageTemplateRequest.TemplateItem env : request.getEnvironment()) { holder.addEnvironment(new EnvironmentTemplateItem(env.getName(), env.getDescription(), env.getSecondaryField())); } } private void addDevices(final ImageTemplateRequest request, final ImageTemplateHolder holder) { for (ImageTemplateRequest.TemplateItem device : request.getDevices()) { holder.addDevice(new DeviceTemplateItem(device.getName(), device.getDescription())); } } private void addMisc(final ImageTemplateRequest request, final ImageTemplateHolder holder) { if (null != request.getCapabilities()) { for (String capability : request.getCapabilities()) { try { holder.addCapability(DockerCapability.valueOf(capability)); } catch (IllegalArgumentException e) { LOGGER.warn("Attempted to add unknown capability {}", capability); } } } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/AbstractAppTask.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; import io.linuxserver.fleet.v2.LoggerOwner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class AbstractAppTask implements AsyncTask, LoggerOwner { private final Logger LOGGER = LoggerFactory.getLogger(getClass()); public final String name; public AbstractAppTask(final String name) { if (null == name) { throw new IllegalArgumentException("name must not be null"); } this.name = name; } @Override public RESPONSE performTaskOn(final DELEGATE delegate) { try { return performTaskInternal(delegate); } catch (Exception e) { LOGGER.error("Unable to complete task", e); throw new TaskExecutionException(e); } } @Override public final Logger getLogger() { return LOGGER; } protected abstract RESPONSE performTaskInternal(final DELEGATE delegate); @Override public String toString() { return "AsyncTask[" + name + "]"; } @Override public int hashCode() { return name.hashCode(); } @Override public boolean equals(Object o) { if (!(o instanceof AbstractAppTask)) { return false; } return ((AbstractAppTask) o).name.equals(name); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/AbstractAppThread.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; import io.linuxserver.fleet.core.FleetAppController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class AbstractAppThread extends Thread { private final Logger LOGGER = LoggerFactory.getLogger(getClass()); private ThreadStatus status = ThreadStatus.Stopped; private final FleetAppController controller; public AbstractAppThread(final FleetAppController controller, final String name) { this.controller = controller; setName(name); } public final FleetAppController getController() { return controller; } @Override public synchronized void run() { try { LOGGER.info("Starting thread..."); status = ThreadStatus.Running; while (isThreadRunning()) { doRunSinglePass(); } } catch (Exception e) { LOGGER.error("Thread has encountered an exception it cannot handle", e); controller.handleException(e); LOGGER.info("Stopping thread..."); status = ThreadStatus.Stopped; } } public final boolean isThreadRunning() { return status == ThreadStatus.Running; } protected final Logger getLogger() { return LOGGER; } protected abstract void doRunSinglePass() throws Exception; } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/AbstractTaskQueueConsumer.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.client.docker.queue.TaskQueue; public abstract class AbstractTaskQueueConsumer> extends AbstractAppThread { private final TaskQueue taskQueue; private final DELEGATE taskDelegate; public AbstractTaskQueueConsumer(final FleetAppController controller, final DELEGATE delegate, final TaskQueue queue, final String consumerThreadName) { super(controller, consumerThreadName); taskQueue = queue; taskDelegate = delegate; } @Override protected void doRunSinglePass() throws Exception { final T task = taskQueue.retrieveNextTask(); try { getLogger().info("Processing single task {}", task); final R response = task.performTaskOn(taskDelegate); handleTaskResponse(response); } catch (TaskExecutionException e) { getLogger().error("Unable to complete the processing of task {}", task, e); } } protected abstract void handleTaskResponse(final R response); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/AsyncTask.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; public interface AsyncTask { RESPONSE performTaskOn(DELEGATE delegate); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/AsyncTaskDelegate.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; import io.linuxserver.fleet.core.FleetAppController; public interface AsyncTaskDelegate { FleetAppController getController(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/AsyncTaskResponse.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; public interface AsyncTaskResponse { void handleResponse(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/TaskExecutionException.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; public class TaskExecutionException extends RuntimeException { public TaskExecutionException(final Exception cause) { super(cause); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/TaskResponseControllerProxy.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; import io.linuxserver.fleet.core.FleetAppController; public class TaskResponseControllerProxy implements AsyncTaskResponse { private final FleetAppController controller; private final R response; public TaskResponseControllerProxy(final FleetAppController controller, final R response) { this.controller = controller; this.response = response; } @Override public void handleResponse() { } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/ThreadStatus.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread; public enum ThreadStatus { Stopped, Running; } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/AbstractAppSchedule.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.LoggerOwner; import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.ScheduleKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.time.LocalDateTime; import java.util.concurrent.atomic.AtomicReference; public abstract class AbstractAppSchedule extends AbstractHasKey implements AppSchedule, LoggerOwner { private final Logger logger = LoggerFactory.getLogger(getClass()); private final FleetAppController controller; private final AtomicReference spec; private final LocalDateTime intantiatedAt; private final AtomicReference lastRun; private final AtomicReference lastRunDuration; public AbstractAppSchedule(final ScheduleSpec spec, final FleetAppController controller) { super(spec.getKey()); this.intantiatedAt = LocalDateTime.now(); this.controller = controller; this.spec = new AtomicReference<>(spec); this.lastRun = new AtomicReference<>(); this.lastRunDuration = new AtomicReference<>(Duration.ZERO); } protected final FleetAppController getController() { return controller; } public final ScheduleSpec getSpec() { return spec.get(); } @Override public final String getName() { return getSpec().getScheduleName(); } @Override public final LocalDateTime getLastRunTime() { return lastRun.get(); } @Override public final LocalDateTime getNextRunTime() { final ScheduleSpec scheduleSpec = getSpec(); final TimeWithUnit interval = scheduleSpec.getInterval(); if (null == getLastRunTime()) { if (getSpec().getDelayOffset().isGreaterThanZero()) { return intantiatedAt.plus(scheduleSpec.getDelayOffset().getTimeDuration(), scheduleSpec.getDelayOffset().getChronoUnit()); } else { return intantiatedAt.plus(interval.getTimeDuration(), interval.getChronoUnit()); } } return getLastRunTime().plus(interval.getTimeDuration(), interval.getChronoUnit()); } @Override public final Duration getLastRunDuration() { return lastRunDuration.get(); } @Override public final TimeWithUnit getInterval() { return getSpec().getInterval(); } @Override public TimeWithUnit getDelay() { return getSpec().getDelayOffset(); } @Override public final Logger getLogger() { return logger; } @Override public void run() { final LocalDateTime startTime = LocalDateTime.now(); final String scheduleName = getSpec().getScheduleName(); try { if (isAllowedToExecute()) { logger.info("Starting run of schedule {}", scheduleName); executeSchedule(); logger.info("Run of schedule {} finished.", scheduleName); } else { logger.info("Schedule is currently not allowed to run. Will log run but will skip until next time."); } } catch (Exception e) { logger.error("Caught unhandled exception during running of schedule {}", scheduleName, e); } final LocalDateTime endTime = LocalDateTime.now(); lastRun.set(endTime); lastRunDuration.set(Duration.between(startTime, endTime)); } @Override public String toString() { return "Name=" + getName() + ", Interval=" + getInterval() + ", InitialDelay=" + getDelay(); } protected boolean isAllowedToExecute() { return true; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/AppSchedule.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule; import io.linuxserver.fleet.v2.key.HasKey; import io.linuxserver.fleet.v2.key.ScheduleKey; import java.time.Duration; import java.time.LocalDateTime; public interface AppSchedule extends HasKey, Runnable { String getName(); LocalDateTime getLastRunTime(); LocalDateTime getNextRunTime(); Duration getLastRunDuration(); TimeWithUnit getDelay(); TimeWithUnit getInterval(); void executeSchedule(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/CheckAppVersionSchedule.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule; import io.linuxserver.fleet.core.FleetAppController; public class CheckAppVersionSchedule extends AbstractAppSchedule { public CheckAppVersionSchedule(final ScheduleSpec spec, final FleetAppController controller) { super(spec, controller); } @Override public void executeSchedule() { getLogger().info("Currently not implemented. This is a placeholder schedule"); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/ScheduleSpec.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule; import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.ScheduleKey; public final class ScheduleSpec extends AbstractHasKey { private final String scheduleName; private final TimeWithUnit interval; private final TimeWithUnit delayOffset; private final Class specForClass; private ScheduleSpec(final ScheduleKey key, final String scheduleName, final TimeWithUnit interval, final TimeWithUnit delayOffset, final Class specForClass) { super(key); this.scheduleName = scheduleName; this.interval = interval; this.delayOffset = delayOffset; this.specForClass = specForClass; } public static ScheduleSpec makeInitial(final ScheduleKey key, final String scheduleName, final TimeWithUnit interval, final TimeWithUnit delayOffset, final Class specForClass) { return new ScheduleSpec(key, scheduleName, interval, delayOffset, specForClass); } public final String getScheduleName() { return scheduleName; } public final TimeWithUnit getInterval() { return interval; } public final TimeWithUnit getDelayOffset() { return delayOffset; } public final Class getScheduleClass() { return specForClass; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/TidyHistoricDataSchedule.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule; import io.linuxserver.fleet.core.FleetAppController; public class TidyHistoricDataSchedule extends AbstractAppSchedule { public TidyHistoricDataSchedule(final ScheduleSpec spec, final FleetAppController controller) { super(spec, controller); } @Override public void executeSchedule() { getLogger().info("Currently not implemented. This is a placeholder schedule"); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/TimeWithUnit.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule; import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; public class TimeWithUnit { public static final TimeWithUnit Zero = new TimeWithUnit(0, TimeUnit.SECONDS); private final long timeDuration; private final TimeUnit timeUnit; public TimeWithUnit(final long timeDuration, final TimeUnit timeUnit) { this.timeDuration = timeDuration; this.timeUnit = timeUnit; } public static TimeWithUnit valueOf(final String value) { if (value.matches("\\d+:(seconds|minutes|hours|days)")) { final String[] values = value.split(":"); return new TimeWithUnit(Integer.parseInt(values[0]), TimeUnit.valueOf(values[1].toUpperCase())); } throw new IllegalArgumentException("Invalid TimeWithUnit value " + value); } public final TimeWithUnit convertToLowestUnit(final TimeWithUnit otherUnit) { if (getTimeUnit().compareTo(otherUnit.getTimeUnit()) < 0) { return this; } return new TimeWithUnit(otherUnit.getTimeUnit().convert(getTimeDuration(), getTimeUnit()), otherUnit.getTimeUnit()); } public final long getTimeDuration() { return timeDuration; } public final ChronoUnit getChronoUnit() { return timeUnit.toChronoUnit(); } public final TimeUnit getTimeUnit() { return timeUnit; } public final boolean isGreaterThanZero() { return getTimeDuration() > 0; } @Override public final String toString() { return getTimeDuration() + ":" + getTimeUnit().name().toLowerCase(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/cache/RefreshCacheSchedule.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule.cache; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.thread.schedule.AbstractAppSchedule; import io.linuxserver.fleet.v2.thread.schedule.ScheduleSpec; public final class RefreshCacheSchedule extends AbstractAppSchedule { public RefreshCacheSchedule(final ScheduleSpec spec, final FleetAppController controller) { super(spec, controller); } @Override public void executeSchedule() { getController().getImageService().reloadCache(); } @Override protected boolean isAllowedToExecute() { return getController().getSynchronisationService().isSyncQueueEmpty(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/sync/AllImagesSyncSchedule.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule.sync; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.thread.schedule.AbstractAppSchedule; import io.linuxserver.fleet.v2.thread.schedule.ScheduleSpec; import io.linuxserver.fleet.v2.types.Repository; import java.util.List; public final class AllImagesSyncSchedule extends AbstractAppSchedule { public AllImagesSyncSchedule(final ScheduleSpec spec, final FleetAppController controller) { super(spec, controller); } @Override public void executeSchedule() { final List allRepositories = getController().getImageService().getAllRepositories(); for (Repository repository : allRepositories) { getController().getSynchronisationService().synchroniseCachedRepository(repository); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/sync/CleanRemovedImagesSchedule.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule.sync; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.thread.schedule.AbstractAppSchedule; import io.linuxserver.fleet.v2.thread.schedule.ScheduleSpec; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.Repository; import io.linuxserver.fleet.v2.types.docker.DockerImage; import java.util.List; import java.util.stream.Collectors; public final class CleanRemovedImagesSchedule extends AbstractAppSchedule { public CleanRemovedImagesSchedule(final ScheduleSpec spec, final FleetAppController controller) { super(spec, controller); } @Override public void executeSchedule() { final List allRepositories = getController().getImageService().getAllRepositories(); for (Repository repository : allRepositories) { getLogger().info("Checking for removed upstream images in " + repository); final List apiImages = getController().getConfiguredDockerDelegate().getImagesForRepository(repository.getKey()); if (apiImages.isEmpty()) { getLogger().warn("executeSchedule found no images for repository " + repository + " upstream. Playing it safe and ignoring clean function."); } else { final List imageNames = apiImages.stream().map(DockerImage::getName).collect(Collectors.toList()); for (Image cachedImage : repository.getImages()) { if (!imageNames.contains(cachedImage.getName())) { getLogger().info("Found removed image upstream. Deleting from cache: " + cachedImage); getController().getImageService().removeImage(cachedImage.getKey()); } } } } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/thread/schedule/sync/GetMissingImagesSchedule.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule.sync; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.thread.schedule.AbstractAppSchedule; import io.linuxserver.fleet.v2.thread.schedule.ScheduleSpec; import io.linuxserver.fleet.v2.types.Repository; import java.util.List; public final class GetMissingImagesSchedule extends AbstractAppSchedule { public GetMissingImagesSchedule(final ScheduleSpec spec, final FleetAppController controller) { super(spec, controller); } @Override public void executeSchedule() { final List cachedRepositories = getController().getImageService().getAllRepositories(); for (Repository repository : cachedRepositories) { getController().getSynchronisationService().synchroniseUpstreamRepository(repository); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/AbstractSyncItem.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.Key; import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec; import java.util.regex.Matcher; import java.util.regex.Pattern; public abstract class AbstractSyncItem> extends AbstractHasKey implements HasSyncSpec { private final ItemSyncSpec syncSpec; public AbstractSyncItem(final KEY key, final ItemSyncSpec syncSpec) { super(key); this.syncSpec = syncSpec; } public abstract ITEM cloneWithSyncSpec(final ItemSyncSpec syncSpec); public final ItemSyncSpec getSpec() { return syncSpec; } @Override public boolean isSyncEnabled() { return getSpec().isSynchronised(); } @Override public boolean isStable() { return getSpec().isStable(); } @Override public boolean isDeprecated() { return getSpec().isDeprecated(); } @Override public String getVersionMask() { return getSpec().getVersionMask(); } @Override public boolean isHidden() { return getSpec().isHidden(); } public final String getMaskedVersion(final Tag tag) { if (null == tag) { return null; } return extractMaskedVersion(tag.getVersion()); } private String extractMaskedVersion(final String tagVersion) { final String versionMask = getVersionMask(); if (null != versionMask) { final Pattern pattern = Pattern.compile(versionMask); final Matcher matcher = pattern.matcher(tagVersion); if (matcher.matches()) { final StringBuilder tagBuilder = new StringBuilder(); for (int groupNum = 1; groupNum <= matcher.groupCount(); groupNum++) tagBuilder.append(matcher.group(groupNum)); return tagBuilder.toString(); } } return tagVersion; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/AppAlert.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.AlertKey; import java.time.LocalDateTime; import java.util.UUID; public class AppAlert extends AbstractHasKey { private final AlertLevel alertLevel; private final LocalDateTime alertDate; private final String subject; private final String alertMessage; private AppAlert(final AlertKey key, final AlertLevel level, final LocalDateTime alertDate, final String subject, final String alertMessage) { super(key); this.alertLevel = level; this.alertDate = LocalDateTime.of(alertDate.toLocalDate(), alertDate.toLocalTime()); this.subject = subject; this.alertMessage = alertMessage; } public static AppAlert makeAlert(final AlertLevel alertLevel, final String subject, final String alertMessage) { return new AppAlert(new AlertKey(UUID.randomUUID().toString()), alertLevel, LocalDateTime.now(), subject, alertMessage); } public final LocalDateTime getAlertDate() { return LocalDateTime.of(alertDate.toLocalDate(), alertDate.toLocalTime()); } public final String getSubject() { return subject; } public final String getAlertMessage() { return alertMessage; } public final AlertLevel getAlertLevel() { return alertLevel; } public final boolean isSystemAlert() { return getAlertLevel().isSystem(); } public enum AlertLevel { Info, Warning, Error, System; public final boolean isInfo() { return this == Info; } public final boolean isSystem() { return this == System; } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/FilePathDetails.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; public class FilePathDetails { private final String fileNameWithExtension; private final String fullAbsolutePath; private final String publicSafePath; public FilePathDetails(final String fileNameWithExtension, final String fullAbsolutePath, final String publicSafePath) { this.fileNameWithExtension = fileNameWithExtension; this.fullAbsolutePath = fullAbsolutePath; this.publicSafePath = publicSafePath; } public final String getFileNameWithExtension() { return fileNameWithExtension; } public final String getFullAbsolutePath() { return fullAbsolutePath; } public final String getPublicSafePathWithFileName() { return publicSafePath + "/" + getFileNameWithExtension(); } public final String getFullAbsolutePathWithFileName() { return getFullAbsolutePath() + "/" + getFileNameWithExtension(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/HasSyncSpec.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; public interface HasSyncSpec { boolean isSyncEnabled(); boolean isStable(); boolean isDeprecated(); String getVersionMask(); boolean isHidden(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/Image.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; import io.linuxserver.fleet.v2.key.HasKey; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.types.meta.ImageMetaData; import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; public class Image extends AbstractSyncItem { private static final DateTimeFormatter DefaultImageTimeFormat = DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss"); private final ImageCountData countData; private final String description; private final LocalDateTime lastUpdated; private final Set tagBranches; private final ImageMetaData metaData; public Image(final ImageKey key, final ItemSyncSpec syncSpec, final ImageMetaData metaData, final ImageCountData countData, final String description, final LocalDateTime lastUpdated) { super(key, syncSpec); this.countData = countData; this.metaData = metaData; this.description = description; this.lastUpdated = parseDateTime(lastUpdated); this.tagBranches = new HashSet<>(); } public final Image cloneForUpdate(final long pullCount, final int starCount, final String description, final LocalDateTime lastUpdated) { final Image cloned = new Image(getKey(), getSpec(), getMetaData(), new ImageCountData(pullCount, starCount), description, lastUpdated); tagBranches.forEach(t -> cloned.addTagBranch(t.cloneForUpdate())); return cloned; } public final Image cloneForUpdate() { return cloneWithSyncSpec(getSpec()); } public final Image cloneWithMetaData(final ImageMetaData metaData) { final Image cloned = new Image(getKey(), getSpec(), metaData, new ImageCountData(getPullCount(), getStarCount()), getDescription(), getLastUpdated()); tagBranches.forEach(t -> cloned.addTagBranch(t.cloneForUpdate())); return cloned; } @Override public final Image cloneWithSyncSpec(final ItemSyncSpec syncSpec) { final Image cloned = new Image(getKey(), syncSpec, getMetaData(), countData, getDescription(), getLastUpdated()); tagBranches.forEach(t -> cloned.addTagBranch(t.cloneForUpdate())); return cloned; } public final String getFullName() { return getRepositoryName() + "/" + getName(); } public final RepositoryKey getRepositoryKey() { return getKey().getRepositoryKey(); } public final String getRepositoryName() { return getRepositoryKey().getName(); } public final String getName() { return getKey().getName(); } public final String getDescription() { return description; } public final ImageMetaData getMetaData() { return metaData; } public final LocalDateTime getLastUpdated() { return parseDateTime(lastUpdated); } public final String getLastUpdatedAsString() { final LocalDateTime lastUpdated = parseDateTime(this.lastUpdated); return null == lastUpdated ? null : DefaultImageTimeFormat.format(lastUpdated); } public final List getTagBranches() { return new ArrayList<>(tagBranches); } public final TagBranch findTagBranchByName(final String branchName) { for (TagBranch tagBranch : tagBranches) { if (tagBranch.getBranchName().equals(branchName)) { return tagBranch; } } return null; } public final void addTagBranch(final TagBranch tagBranch) { final boolean added = tagBranches.add(tagBranch); if (!added) { throw new IllegalArgumentException("TagBranch " + tagBranch + " already present in Image"); } } public final void removeTagBranch(final TagBranch tagBranch) { final Iterator iterator = tagBranches.iterator(); while (iterator.hasNext()) { final TagBranch current = iterator.next(); if (current.equals(tagBranch) && !current.isBranchProtected()) { iterator.remove(); } } } public final long getPullCount() { return countData.getPullCount(); } public final int getStarCount() { return countData.getStarCount(); } public final Tag getLatestTag() { for (TagBranch storedTagBranch : getTagBranches()) { if (storedTagBranch.isNamedLatest()) { return storedTagBranch.getLatestTag(); } } return Tag.DefaultUnknown; } @Override public final int compareTo(final HasKey o) { return getKey().getName().compareTo(o.getKey().getName()); } @Override public String toString() { return getRepositoryName() + "/" + getName() + "@" + getLatestTag(); } private LocalDateTime parseDateTime(final LocalDateTime dateTime) { return null == dateTime ? null : LocalDateTime.of(dateTime.toLocalDate(), dateTime.toLocalTime()); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/ImageCountData.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; public class ImageCountData { private final long pullCount; private final int starCount; public ImageCountData(final long pullCount, final int starCount) { this.pullCount = pullCount; this.starCount = starCount; } public final long getPullCount() { return pullCount; } public final int getStarCount() { return starCount; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/Repository.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; import io.linuxserver.fleet.v2.cache.ImageCache; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class Repository extends AbstractSyncItem { private final ImageCache images; public Repository(final RepositoryKey key, final ItemSyncSpec syncSpec) { super(key, syncSpec); images = new ImageCache(); } @Override public Repository cloneWithSyncSpec(final ItemSyncSpec syncSpec) { final Repository cloned = new Repository(getKey(), syncSpec); images.getAllItems().forEach(i -> addImage(i.cloneWithSyncSpec(i.getSpec()))); return cloned; } public final void addImage(final Image image) { images.addItem(image); } public final String getName() { return getKey().getName(); } public final List getImages() { final List imageList = new ArrayList<>(images.getAllItems()); Collections.sort(imageList); return imageList; } public final long getTotalPulls() { long totalPulls = 0; for (Image image : getImages()) { totalPulls += image.getPullCount(); } return totalPulls; } public final int getTotalStars() { int totalStars = 0; for (Image image : getImages()) { totalStars += image.getStarCount(); } return totalStars; } @Override public final boolean isHidden() { return !isSyncEnabled(); } @Override public final boolean isStable() { return true; } @Override public final boolean isDeprecated() { return false; } public final void removeImage(final Image image) { images.removeItem(image.getKey()); } @Override public final String toString() { return getName() + "[nImages=" + images.size() + "]"; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/Tag.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; import java.time.LocalDateTime; import java.util.*; public class Tag { public static final Tag DefaultUnknown = new Tag("Unknown", null, Collections.emptySet()); private final String version; private final Set digests; private final LocalDateTime buildDate; public Tag(final String version, final LocalDateTime buildDate, final Set digests) { this.version = version; this.digests = Collections.unmodifiableSet(digests); this.buildDate = (null == buildDate ? null : LocalDateTime.of(buildDate.toLocalDate(), buildDate.toLocalTime())); } public final List getDigests() { return new ArrayList<>(digests); } public String getVersion() { return version; } public LocalDateTime getBuildDate() { if (null != buildDate) { return LocalDateTime.of(buildDate.toLocalDate(), buildDate.toLocalTime()); } return null; } @Override public String toString() { return version; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/TagBranch.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.TagBranchKey; import java.util.concurrent.atomic.AtomicReference; public class TagBranch extends AbstractHasKey { private final String branchName; private final AtomicReference branchProtected; private final AtomicReference latestTag; public TagBranch(final TagBranchKey tagBranchKey, final String tagBranchName, final boolean branchProtected, final Tag latestTag) { super(tagBranchKey); this.branchName = tagBranchName; this.branchProtected = new AtomicReference<>(branchProtected); this.latestTag = new AtomicReference<>(latestTag); } public final void updateLatestTag(final Tag latestTag) { this.latestTag.set(latestTag); } public final void setBranchProtected(final boolean branchProtected) { this.branchProtected.set(branchProtected); } public final String getBranchName() { return branchName; } public final Tag getLatestTag() { return latestTag.get(); } public final boolean isNamedLatest() { return "latest".equals(getBranchName()); } public final boolean isBranchProtected() { return branchProtected.get(); } public final TagBranch cloneForUpdate() { return new TagBranch(getKey(), getBranchName(), isBranchProtected(), getLatestTag()); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/TagDigest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; import java.util.Objects; public class TagDigest implements Comparable { private final long size; private final String digest; private final String architecture; private final String archVariant; public TagDigest(final long size, final String digest, final String architecture, final String archVariant) { this.size = size; this.digest = digest; this.architecture = architecture; this.archVariant = archVariant; } public final long getSize() { return size; } public final String getDigest() { return digest; } public final String getArchitecture() { return architecture; } public final String getArchVariant() { return archVariant; } @Override public int hashCode() { return Objects.hash(digest, architecture, archVariant); } @Override public boolean equals(Object obj) { if (null == obj) { return false; } if (!(obj instanceof TagDigest)) { return false; } final TagDigest other = (TagDigest) obj; return Objects.equals(digest, other.digest) && Objects.equals(architecture, other.architecture) && Objects.equals(archVariant, other.archVariant); } @Override public String toString() { return "DIGEST:" + digest + "--" + architecture + "/" + archVariant; } @Override public int compareTo(TagDigest o) { if (null == o) { return 1; } return toString().compareTo(o.toString()); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/User.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types; import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.UserKey; import io.linuxserver.fleet.v2.web.AppRole; import java.time.LocalDateTime; public class User extends AbstractHasKey { private final String username; private final String password; private final AppRole role; private final LocalDateTime modifiedTime; public User(final UserKey key, final String username, final String password, final LocalDateTime modifiedTime, final AppRole role) { super(key); this.username = username; this.password = password; this.modifiedTime = LocalDateTime.of(modifiedTime.toLocalDate(), modifiedTime.toLocalTime()); this.role = role; } public final String getUsername() { return username; } public final String getPassword() { return password; } public final AppRole getRole() { return role; } public final LocalDateTime getModifiedTime() { if (null != modifiedTime) { return LocalDateTime.of(modifiedTime.toLocalDate(), modifiedTime.toLocalTime()); } return null; } public final User cloneWithPassword(final String hashedPassword) { return new User(getKey(), username, hashedPassword, LocalDateTime.now(), role); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/AbstractApiWrapper.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api; public class AbstractApiWrapper { private final T originalObject; public AbstractApiWrapper(final T originalObject) { this.originalObject = originalObject; } protected final T getOriginalObject() { return originalObject; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/ApiImagePullHistoryWrapper.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api; import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic; import java.util.*; import java.util.stream.Collectors; public class ApiImagePullHistoryWrapper extends AbstractApiWrapper> { private final ImagePullStatistic.StatGroupMode groupMode; public ApiImagePullHistoryWrapper(final List originalObject, final ImagePullStatistic.StatGroupMode groupMode) { super(originalObject); this.groupMode = groupMode; } public final String getGroupModeDataPoint() { return groupMode.getDataPoint(); } public final List getLabels() { return getOriginalObject().stream().map(ImagePullStatistic::getGroupedDateTime).collect(Collectors.toList()); } public final List getPulls() { return getOriginalObject().stream().map(ImagePullStatistic::getPullCount).collect(Collectors.toList()); } public final long getMean() { return (long) getPullDifferential().getPulls().stream().mapToLong(Long::longValue).average().orElse(0.0); } public final PullDifferentialsWithLabels getPullDifferential() { final PullDifferentialsWithLabels differentialsWithLabels = new PullDifferentialsWithLabels(); int i; for (i = 1; i < (getOriginalObject().size() - 1); i++) { final ImagePullStatistic previousStat = getOriginalObject().get(i - 1); final ImagePullStatistic currentStat = getOriginalObject().get(i); differentialsWithLabels.addDifferential(currentStat.getGroupedDateTime(), (currentStat.getPullCount() - previousStat.getPullCount())); } return differentialsWithLabels; } public static class PullDifferentialsWithLabels { private final Map differentials; public PullDifferentialsWithLabels() { differentials = new TreeMap<>(); } public final void addDifferential(final String label, final Long pulls) { differentials.put(label, pulls); } public final Set getLabels() { return differentials.keySet(); } public final Collection getPulls() { return differentials.values(); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/ApiImageWrapper.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api; import io.linuxserver.fleet.v2.types.Image; public class ApiImageWrapper extends AbstractApiWrapper { public ApiImageWrapper(final Image originalObject) { super(originalObject); } public final String getName() { return getOriginalObject().getName(); } public final String getVersionMask() { return getOriginalObject().getVersionMask(); } public final boolean isSyncEnabled() { return getOriginalObject().isSyncEnabled(); } public final boolean isHidden() { return getOriginalObject().isHidden(); } public final boolean isDeprecated() { return getOriginalObject().isDeprecated(); } public final boolean isStable() { return getOriginalObject().isStable(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/ApiRepositoryWrapper.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api; import io.linuxserver.fleet.v2.types.Repository; public class ApiRepositoryWrapper extends AbstractApiWrapper { public ApiRepositoryWrapper(final Repository originalObject) { super(originalObject); } public final String getName() { return getOriginalObject().getName(); } public final String getVersionMask() { return getOriginalObject().getVersionMask(); } public final boolean isSyncEnabled() { return getOriginalObject().isSyncEnabled(); } public final int getNumberOfImages() { return getOriginalObject().getImages().size(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/ApiScheduleWrapper.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api; import io.linuxserver.fleet.v2.thread.schedule.AppSchedule; public class ApiScheduleWrapper extends AbstractApiWrapper { public ApiScheduleWrapper(final AppSchedule schedule) { super(schedule); } public final String getName() { return getOriginalObject().getName(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/external/AllImagesExternalApiResponse.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api.external; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class AllImagesExternalApiResponse { private final Map> repositories; public AllImagesExternalApiResponse() { this.repositories = new HashMap<>(); } public final ExternalApiImage addImage(final String repositoryName, final String imageName, final long pullCount, final String version, final String category, final boolean stable, final boolean deprecated) { if (!repositories.containsKey(repositoryName)) { repositories.put(repositoryName, new ArrayList<>()); } final ExternalApiImage apiImage = new ExternalApiImage(imageName, pullCount, version, category, stable, deprecated); repositories.get(repositoryName).add(apiImage); return apiImage; } public final long getTotalPullCount() { long totalPullCount = 0L; for (List repositoryImages : repositories.values()) { for (ExternalApiImage image : repositoryImages) { totalPullCount += image.getPullCount(); } } return totalPullCount; } public final Map> getRepositories() { return repositories; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/external/ExternalApiImage.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api.external; import io.linuxserver.fleet.v2.types.api.external.templates.ApiTemplateHolder; public class ExternalApiImage { private final String name; private final long pullCount; private final String version; private final String category; private final boolean stable; private final boolean deprecated; private ApiTemplateHolder templateSpec; public ExternalApiImage(final String name, final long pullCount, final String version, final String category, final boolean stable, final boolean deprecated) { this.name = name; this.pullCount = pullCount; this.version = version; this.category = category; this.stable = stable; this.deprecated = deprecated; } public final String getName() { return name; } public final long getPullCount() { return pullCount; } public final String getVersion() { return version; } public final String getCategory() { return category; } public final boolean isStable() { return stable; } public final boolean isDeprecated() { return deprecated; } public final void setTemplateSpec(final ApiTemplateHolder templateHolder) { this.templateSpec = templateHolder; } public final ApiTemplateHolder getTemplateSpec() { return templateSpec; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/external/ExternalApiResponse.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api.external; public class ExternalApiResponse { private ApiStatus status; private T data; public ExternalApiResponse(final ApiStatus status, final T data) { this.status = status; this.data = data; } public final ApiStatus getStatus() { return status; } public final T getData() { return data; } public enum ApiStatus { OK, Error } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/external/templates/ApiDeviceTemplate.java ================================================ /* * Copyright (c) 2021 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api.external.templates; public class ApiDeviceTemplate { private final String device; private final String description; public ApiDeviceTemplate(final String device, final String description) { this.device = device; this.description = description; } public String getDevice() { return device; } public String getDescription() { return description; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/external/templates/ApiEnvTemplate.java ================================================ /* * Copyright (c) 2021 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api.external.templates; public class ApiEnvTemplate { private final String name; private final String exampleValue; private final String description; public ApiEnvTemplate(final String name, final String exampleValue, final String description) { this.name = name; this.exampleValue = exampleValue; this.description = description; } public String getName() { return name; } public String getExampleValue() { return exampleValue; } public String getDescription() { return description; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/external/templates/ApiPortTemplate.java ================================================ /* * Copyright (c) 2021 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api.external.templates; public class ApiPortTemplate { private final int port; private final String protocol; private final String description; public ApiPortTemplate(final int port, final String protocol, final String description) { this.port = port; this.protocol = protocol; this.description = description; } public int getPort() { return port; } public String getProtocol() { return protocol; } public String getDescription() { return description; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/external/templates/ApiTemplateHolder.java ================================================ /* * Copyright (c) 2021 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api.external.templates; import java.util.ArrayList; import java.util.List; public class ApiTemplateHolder { private final List ports = new ArrayList<>(); private final List volumes = new ArrayList<>(); private final List environmentVariables = new ArrayList<>(); private final List devices = new ArrayList<>(); private final List capabilities = new ArrayList<>(); public boolean hostNetwork; public boolean privileged; public ApiTemplateHolder(final boolean hostNetwork, final boolean privileged) { this.hostNetwork = hostNetwork; this.privileged = privileged; } public final void addCapability(final String cap) { capabilities.add(cap); } public final void addDevice(final ApiDeviceTemplate deviceTemplate) { devices.add(deviceTemplate); } public final void addEnv(final ApiEnvTemplate envTemplate) { environmentVariables.add(envTemplate); } public final void addPort(final ApiPortTemplate portTemplate) { ports.add(portTemplate); } public final void addVolume(final ApiVolumeTemplate volumeTemplate) { volumes.add(volumeTemplate); } public List getCapabilities() { return capabilities; } public List getDevices() { return devices; } public List getEnvironmentVariables() { return environmentVariables; } public List getPorts() { return ports; } public List getVolumes() { return volumes; } public boolean isHostNetwork() { return hostNetwork; } public boolean isPrivileged() { return privileged; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/api/external/templates/ApiVolumeTemplate.java ================================================ /* * Copyright (c) 2021 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.api.external.templates; public class ApiVolumeTemplate { private final String containerPath; private final boolean readonly; private final String description; public ApiVolumeTemplate(final String containerPath, final boolean readonly, final String description) { this.containerPath = containerPath; this.readonly = readonly; this.description = description; } public String getContainerPath() { return containerPath; } public boolean isReadonly() { return readonly; } public String getDescription() { return description; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/docker/DockerCapability.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.docker; public enum DockerCapability { AUDIT_CONTROL, AUDIT_WRITE, BLOCK_SUSPEND, CHOWN, DAC_OVERRIDE, DAC_READ_SEARCH, FOWNER, FSETID, IPC_LOCK, IPC_OWNER, KILL, LEASE, LINUX_IMMUTABLE, MAC_ADMIN, MAC_OVERRIDE, MKNOD, NET_ADMIN, NET_BIND_SERVICE, NET_BROADCAST, NET_RAW, SETFCAP, SETGID, SETPCAP, SETUID, SYSLOG, SYS_ADMIN, SYS_BOOT, SYS_CHROOT, SYS_MODULE, SYS_NICE, SYS_PACCT, SYS_PTRACE, SYS_RAWIO, SYS_RESOURCE, SYS_TIME, SYS_TTY_CONFIG, WAKE_ALARM; } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/docker/DockerImage.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.docker; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; /** *

* Fleet's specifically required structure for an image coming from DockerHub. This object * is common within the app, and is immune to any API changes to DockerHub itself. *

*/ public class DockerImage { private final String name; private final String repository; private final String description; private final int starCount; private final long pullCount; private final LocalDateTime buildDate; private List tags = new ArrayList<>(); public DockerImage(String name, String repository, String description, int starCount, long pullCount, LocalDateTime buildDate) { this.name = name; this.repository = repository; this.description = description; this.starCount = starCount; this.pullCount = pullCount; this.buildDate = buildDate; } public final void addTag(final DockerTag tag) { tags.add(tag); } public final List getTags() { return tags; } public final String getName() { return name; } public final String getRepository() { return repository; } public final String getDescription() { return description; } public final int getStarCount() { return starCount; } public final long getPullCount() { return pullCount; } public final LocalDateTime getBuildDate() { return buildDate; } @Override public final String toString() { return getRepository() + "/" + getName(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/docker/DockerTag.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.docker; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; public class DockerTag { private final String name; private final long size; private final LocalDateTime buildDate; private final List digests = new ArrayList<>(); public DockerTag(String name, long size, LocalDateTime buildDate) { this.name = name; this.size = size; this.buildDate = buildDate; } public final void addDigest(final DockerTagManifestDigest digest) { digests.add(digest); } public final List getDigests() { return digests; } public String getName() { return name; } public long getSize() { return size; } public LocalDateTime getBuildDate() { return buildDate; } @Override public final String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/docker/DockerTagManifestDigest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.docker; public class DockerTagManifestDigest { private final long size; private final String digest; private final String architecture; private final String archVariant; public DockerTagManifestDigest(final long size, final String digest, final String architecture, final String archVariant) { this.size = size; this.digest = digest; this.architecture = architecture; this.archVariant = archVariant; } public final long getSize() { return size; } public final String getDigest() { return digest; } public final String getArchitecture() { return architecture; } public final String getArchVariant() { return archVariant; } @Override public final String toString() { return architecture + "/" + archVariant + "[" + digest + "]"; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/AbstractParamRequest.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Stream; public class AbstractParamRequest { private final Map> params; public AbstractParamRequest(final Map> params) { this.params = params; } protected final List getParams(final String key) { return params.get(key); } protected final String getOrNull(final String value) { return "".equalsIgnoreCase(value.trim()) ? null : value; } protected final String getFirstOrNull(final String key) { final List strings = params.get(key); if (null == strings || strings.isEmpty()) { return null; } return strings.get(0); } protected final boolean getAsBoolean(final String value) { return "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value); } @SafeVarargs protected final void checkLists(final List... lists) { boolean containsDifferent = false; boolean allNull = Stream.of(lists).allMatch(Objects::isNull); boolean noneNull = Stream.of(lists).allMatch(Objects::nonNull); if (allNull || noneNull) { if (allNull) { return; } } else { containsDifferent = true; } if (!containsDifferent) { int prevSize = -1; for (List list : lists) { if (prevSize != -1 && list.size() != prevSize) { containsDifferent = true; break; } else { prevSize = list.size(); } } } if (containsDifferent) { throw new IllegalArgumentException("One or more values are null when others are not, or sizes mismatch"); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/ImageAppLogo.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; import io.linuxserver.fleet.v2.key.ImageKey; import java.io.InputStream; public class ImageAppLogo { private final ImageKey imageKey; private final InputStream rawDataStream; private final String mimeType; private final String logoName; private final long logoSize; private final String fileExtension; public ImageAppLogo(final ImageKey imageKey, final InputStream rawDataStream, final String mimeType, final String logoName, final long logoSize, final String fileExtension) { this.imageKey = imageKey; this.rawDataStream = rawDataStream; this.mimeType = mimeType; this.logoName = logoName; this.logoSize = logoSize; this.fileExtension = fileExtension; } public final ImageKey getImageKey() { return imageKey; } public final InputStream getRawDataStream() { return rawDataStream; } public final String getMimeType() { return mimeType; } public final String getLogoName() { return logoName; } public final long getLogoSize() { return logoSize; } public final String getFileExtension() { return fileExtension; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/ImageGeneralInfoUpdateRequest.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; import java.util.List; import java.util.Map; public class ImageGeneralInfoUpdateRequest extends AbstractParamRequest { private final ImageAppLogo imageAppLogo; public ImageGeneralInfoUpdateRequest(final Map> params, final ImageAppLogo imageAppLogo) { super(params); this.imageAppLogo = imageAppLogo; } public final ImageAppLogo getImageAppLogo() { return imageAppLogo; } public final String getBaseImage() { return getFirstOrNull("ImageBase"); } public final String getCategory() { return getFirstOrNull("ImageCategory"); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/ImageOutlineRequest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; import io.linuxserver.fleet.v2.key.RepositoryKey; import java.time.LocalDateTime; public class ImageOutlineRequest { private final RepositoryKey repositoryKey; private final String imageName; private final String imageDescription; private final LocalDateTime imageLastUpdated; public ImageOutlineRequest(final RepositoryKey repositoryKey, final String imageName, final String imageDescription, final LocalDateTime imageLastUpdated) { this.repositoryKey = repositoryKey; this.imageName = imageName; this.imageDescription = imageDescription; this.imageLastUpdated = imageLastUpdated; } public final RepositoryKey getRepositoryKey() { return repositoryKey; } public final String getImageName() { return imageName; } public final String getImageDescription() { return imageDescription; } public final LocalDateTime getImageLastUpdated() { return imageLastUpdated; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/ImageTemplateRequest.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; import java.util.ArrayList; import java.util.List; import java.util.Map; public class ImageTemplateRequest extends AbstractParamRequest { public ImageTemplateRequest(final Map> rawTemplateParams) { super(rawTemplateParams); } public final String getRegistryUrl() { return getFirstOrNull("ImageTemplateUpstreamUrl"); } public final String getRestartPolicy() { return getFirstOrNull("ImageTemplateRestartPolicy"); } public final boolean isHostNetworkEnabled() { return getAsBoolean(getFirstOrNull("ImageTemplateNetworkHost")); } public final boolean isPrivilegedMode() { return getAsBoolean(getFirstOrNull("ImageTemplatePrivileged")); } public final List getCapabilities() { return getParams("ImageTemplateCapabilities"); } public final List> getPorts() { final List portNumbers = getParams("imageTemplatePort"); final List portProtocols = getParams("imageTemplatePortProtocol"); final List portDescriptions = getParams("imageTemplatePortDescription"); checkLists(portNumbers, portProtocols, portDescriptions); final List> ports = new ArrayList<>(); if (null != portNumbers) { int i = 0; for (; i < portNumbers.size(); i++) { ports.add(new TemplateItem<>(portNumbers.get(i), getOrNull(portDescriptions.get(i)), portProtocols.get(i))); } } return ports; } public final List> getVolumes() { final List volumeNames = getParams("imageTemplateVolume"); final List volumeReadOnlys = getParams("imageTemplateVolumeReadonly"); final List volumeDescriptions = getParams("imageTemplateVolumeDescription"); checkLists(volumeNames, volumeReadOnlys, volumeDescriptions); final List> volumes = new ArrayList<>(); if (null != volumeNames) { int i = 0; for (; i < volumeNames.size(); i++) { volumes.add(new TemplateItem<>(volumeNames.get(i), getOrNull(volumeDescriptions.get(i)), "readonly".equalsIgnoreCase(volumeReadOnlys.get(i)))); } } return volumes; } public final List> getEnvironment() { final List envNames = getParams("imageTemplateEnv"); final List envDescriptions = getParams("imageTemplateEnvDescription"); final List envExamples = getParams("imageTemplateEnvExample"); checkLists(envNames, envDescriptions, envExamples); final List> env = new ArrayList<>(); if (null != envNames) { int i = 0; for (; i < envNames.size(); i++) { env.add(new TemplateItem<>(envNames.get(i), getOrNull(envDescriptions.get(i)), getOrNull(envExamples.get(i)))); } } return env; } public final List> getDevices() { final List deviceNames = getParams("imageTemplateDevice"); final List deviceDescriptions = getParams("imageTemplateDeviceDescription"); checkLists(deviceNames, deviceDescriptions); final List> env = new ArrayList<>(); if (null != deviceNames) { int i = 0; for (; i < deviceNames.size(); i++) { env.add(new TemplateItem<>(deviceNames.get(i), getOrNull(deviceDescriptions.get(i)),null)); } } return env; } public static class TemplateItem { private final String name; private final String description; private final T secondaryField; public TemplateItem(final String name, final String description, final T secondaryField) { this.name = name; this.description = description; this.secondaryField = secondaryField; } public final String getName() { return name; } public final String getDescription() { return description; } public final T getSecondaryField() { return secondaryField; } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/ImageUrlsUpdateRequest.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; import io.linuxserver.fleet.v2.types.meta.ExternalUrl; import io.linuxserver.fleet.v2.types.meta.ExternalUrlKey; import java.util.ArrayList; import java.util.List; import java.util.Map; public class ImageUrlsUpdateRequest extends AbstractParamRequest { public ImageUrlsUpdateRequest(final Map> params) { super(params); } public final List getExternalUrls() { final List urlKeys = getParams("imageExternalUrlKey"); final List urlTypes = getParams("imageExternalUrlType"); final List urlNames = getParams("imageExternalUrlName"); final List urlPaths = getParams("imageExternalUrlPath"); checkLists(urlKeys, urlTypes, urlNames, urlPaths); final List urls = new ArrayList<>(); if (null != urlKeys) { int i = 0; for (; i < urlKeys.size(); i++) { urls.add(new ExternalUrl(new ExternalUrlKey(Integer.parseInt(urlKeys.get(i))), ExternalUrl.ExternalUrlType.valueOf(urlTypes.get(i)), urlNames.get(i), urlPaths.get(i))); } } return urls; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/RepositoryOutlineRequest.java ================================================ /*io.linuxserver.fleet.v2.db * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; public class RepositoryOutlineRequest { private final String repositoryName; public RepositoryOutlineRequest(final String repositoryName) { this.repositoryName = repositoryName; } public final String getRepositoryName() { return repositoryName; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/TagBranchOutlineRequest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; import io.linuxserver.fleet.v2.key.ImageKey; public class TagBranchOutlineRequest { private final ImageKey imageKey; private final String branchName; public TagBranchOutlineRequest(final ImageKey imageKey, final String branchName) { this.imageKey = imageKey; this.branchName = branchName; } public final ImageKey getImageKey() { return imageKey; } public final String getBranchName() { return branchName; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/internal/UserOutlineRequest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.internal; import io.linuxserver.fleet.v2.web.AppRole; public class UserOutlineRequest { public static final UserOutlineRequest InitialFirstLoadUser = new UserOutlineRequest("admin", "admin", AppRole.Admin); private final String username; private final String password; private final AppRole role; public UserOutlineRequest(final String username, final String password, final AppRole role) { this.username = username; this.password = password; this.role = role; } public final UserOutlineRequest cloneWithPassword(final String password) { return new UserOutlineRequest(getUsername(), password, getRole()); } public final String getUsername() { return username; } public final String getPassword() { return password; } public final AppRole getRole() { return role; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/ExternalUrl.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta; import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.HasKey; public class ExternalUrl extends AbstractHasKey { private final ExternalUrlType type; private final String name; private final String absoluteUrl; public ExternalUrl(final ExternalUrlKey key, final ExternalUrlType type, final String name, final String absoluteUrl) { super(key); this.type = type; this.name = name; this.absoluteUrl = absoluteUrl; } public final ExternalUrlType getType() { return type; } public final String getName() { return name; } public final String getAbsoluteUrl() { return absoluteUrl; } @Override public final String toString() { return type + "[" + name + "]{" + absoluteUrl + "}"; } @Override public final int compareTo(final HasKey o) { if (isNonPersisted(getKey()) && isNonPersisted(o.getKey())) { return -1; } return getKey().getId().compareTo(o.getKey().getId()); } private boolean isNonPersisted(final ExternalUrlKey key) { return key.getId().equals(ExternalUrlKey.NewNotPersistedYet.getId()); } public enum ExternalUrlType { Support( "life-ring", "Support information relating to setup, debugging, or other issues"), Application("server", "Primary link to application source or marketing"), Donation( "donate", "Accepted donations"), Misc( "link", "Other external resource"); private final String icon; private final String description; ExternalUrlType(final String icon, final String description) { this.icon = icon; this.description = description; } public final String getIcon() { return icon; } public final String getDescription() { return description; } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/ExternalUrlKey.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta; import io.linuxserver.fleet.v2.Utils; import io.linuxserver.fleet.v2.key.AbstractDatabaseKey; import io.linuxserver.fleet.v2.key.Key; public class ExternalUrlKey extends AbstractDatabaseKey { public static final ExternalUrlKey NewNotPersistedYet = new ExternalUrlKey(-1); public ExternalUrlKey(final Integer id) { super(Utils.ensureNotNull(id)); } @Override public boolean equals(Object o) { final boolean keysMath = super.equals(o); final Key other = (Key) o; if (keysMath && areBothNonPersistedKeys(other)) { return false; } return keysMath; } private boolean areBothNonPersistedKeys(Key other) { return other.getId().equals(NewNotPersistedYet.getId()) && getId().equals(NewNotPersistedYet.getId()); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/ImageCoreMeta.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; public class ImageCoreMeta { private final String appImagePath; private final String baseImage; private final String category; private final Set externalUrls; public ImageCoreMeta(final String appImagePath, final String baseImage, final String category) { this.appImagePath = appImagePath; this.baseImage = baseImage; this.category = category; this.externalUrls = new TreeSet<>(); } public final ImageCoreMeta cloneWithBaseData(final String appImagePath, final String baseImage, final String category) { final ImageCoreMeta cloned = new ImageCoreMeta(appImagePath, baseImage, category); externalUrls.forEach(cloned::addExternalUrl); return cloned; } public final ImageCoreMeta cloneWithExternalUrls(final List externalUrls) { final ImageCoreMeta cloned = new ImageCoreMeta(appImagePath, baseImage, category); externalUrls.forEach(cloned::addExternalUrl); return cloned; } public final void addExternalUrl(final ExternalUrl externalUrl) { final boolean added = externalUrls.add(externalUrl); if (!added) { throw new IllegalArgumentException("External Url already present: " + externalUrl); } } public final void removeExternalUrl(final ExternalUrlKey externalUrlKey) { externalUrls.removeIf(url -> url.getKey().equals(externalUrlKey)); } public final String getAppImagePath() { return appImagePath; } public final String getBaseImage() { return baseImage; } public final String getCategory() { return category; } public final List getExternalUrls() { return new ArrayList<>(externalUrls); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/ImageMetaData.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta; import io.linuxserver.fleet.v2.types.meta.history.ImagePullHistory; import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic; import io.linuxserver.fleet.v2.types.meta.template.ImageTemplateHolder; import java.util.List; public class ImageMetaData { private final ImagePullHistory pullHistory; private final ImageTemplateHolder templateHolder; private final ImageCoreMeta coreMeta; public ImageMetaData(final ImageCoreMeta coreMeta, final ImagePullHistory pullHistory, final ImageTemplateHolder templateHolder) { this.coreMeta = coreMeta; this.pullHistory = pullHistory; this.templateHolder = templateHolder; } public final ImageMetaData cloneWithTemplate(final ImageTemplateHolder templateHolder) { return new ImageMetaData(getCoreMeta(), pullHistory, templateHolder); } public final ImageMetaData cloneWithCoreMeta(final ImageCoreMeta coreMeta) { return new ImageMetaData(coreMeta, pullHistory, getTemplates()); } public final List getHistoryFor(final ImagePullStatistic.StatGroupMode groupMode) { return pullHistory.getHistoryFor(groupMode); } public final ImageTemplateHolder getTemplates() { return templateHolder; } public final ImageCoreMeta getCoreMeta() { return coreMeta; } public final String getAppImagePath() { return getCoreMeta().getAppImagePath(); } public final String getBaseImage() { return getCoreMeta().getBaseImage(); } public final String getCategory() { return getCoreMeta().getCategory(); } public final List getExternalUrls() { return getCoreMeta().getExternalUrls(); } public final boolean isPopulated() { return (null != getCategory() && !getCategory().isBlank()) || !getExternalUrls().isEmpty(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/ItemSyncSpec.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta; public class ItemSyncSpec { public static final ItemSyncSpec Default = new ItemSyncSpec(false, false, true, true, null); private boolean deprecated; private boolean hidden; private boolean stable; private boolean synchronised; private String versionMask; public ItemSyncSpec(final boolean deprecated, final boolean hidden, final boolean stable, final boolean synchronised, final String versionMask) { setDeprecated(deprecated); setHidden(hidden); setStable(stable); setSynchronised(synchronised); setVersionMask(versionMask); } public final ItemSyncSpec copyOf() { return new ItemSyncSpec(isDeprecated(), isHidden(), isStable(), isSynchronised(), getVersionMask()); } public final void setDeprecated(boolean deprecated) { this.deprecated = deprecated; } public final void setHidden(boolean hidden) { this.hidden = hidden; } public final void setStable(boolean stable) { this.stable = stable; } public final void setSynchronised(boolean synchronised) { this.synchronised = synchronised; } public final void setVersionMask(String versionMask) { this.versionMask = versionMask; } public final boolean isDeprecated() { return deprecated; } public final boolean isHidden() { return hidden; } public final boolean isStable() { return stable; } public final boolean isSynchronised() { return synchronised; } public final String getVersionMask() { return versionMask; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/history/ImagePullHistory.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.history; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; public class ImagePullHistory { private final Set historicalPulls; public ImagePullHistory() { historicalPulls = new TreeSet<>(); } public final boolean addStatistic(final ImagePullStatistic statistic) { return historicalPulls.add(statistic); } public final List getHistoryFor(final ImagePullStatistic.StatGroupMode groupMode) { return historicalPulls.stream().filter(s -> s.isGroupedBy(groupMode)).collect(Collectors.toList()); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/history/ImagePullStatistic.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.history; public class ImagePullStatistic implements Comparable { private final long pullCount; private final String groupedDateTime; private final StatGroupMode groupMode; public ImagePullStatistic(final long pullCount, final String groupedDateTime, final StatGroupMode groupMode) { this.pullCount = pullCount; this.groupedDateTime = groupedDateTime; this.groupMode = groupMode; } public final long getPullCount() { return pullCount; } public final String getGroupedDateTime() { return groupedDateTime; } public final boolean isGroupedBy(final StatGroupMode groupMode) { return this.groupMode == groupMode; } @Override public int compareTo(final ImagePullStatistic o) { final int dateComparison = groupedDateTime.compareTo(o.groupedDateTime); if (dateComparison == 0) { return groupMode.compareTo(o.groupMode); } return dateComparison; } @Override public int hashCode() { return groupMode.hashCode() + groupedDateTime.hashCode(); } @Override public boolean equals(final Object obj) { if (!(obj instanceof ImagePullStatistic)) { return false; } final ImagePullStatistic other = (ImagePullStatistic) obj; return other.groupedDateTime.equals(groupedDateTime) && other.isGroupedBy(groupMode); } @Override public String toString() { return pullCount + "@" + groupedDateTime + ":" + groupMode; } public enum StatGroupMode { Day("hour"), Week("day"), Month("day"); private final String dataPoints; StatGroupMode(final String dataPoints) { this.dataPoints = dataPoints; } public final String getDataPoint() { return dataPoints; } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/template/AbstractTemplateItem.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.template; public abstract class AbstractTemplateItem, ITEM extends AbstractTemplateItem> implements TemplateItem, Comparable { private final T name; private final String description; protected AbstractTemplateItem(final T name, final String description) { this.name = name; this.description = description; } @Override public final T getName() { return name; } @Override public final String getDescription() { return description; } @Override public int compareTo(final ITEM o) { return o.getName().compareTo(getName()); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/template/DeviceTemplateItem.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.template; public class DeviceTemplateItem extends AbstractTemplateItem { public DeviceTemplateItem(final String name, final String description) { super(name, description); } public final String getDevice() { return getName(); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/template/EnvironmentTemplateItem.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.template; public class EnvironmentTemplateItem extends AbstractTemplateItem { private final String exampleValue; public EnvironmentTemplateItem(final String name, final String description, final String exampleValue) { super(name, description); this.exampleValue = exampleValue; } public final String getEnv() { return getName(); } public final String getExampleValue() { return exampleValue; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/template/ImageTemplateHolder.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.template; import io.linuxserver.fleet.v2.types.docker.DockerCapability; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; public class ImageTemplateHolder { // Mappings private final Set ports = new TreeSet<>(); private final Set volumes = new TreeSet<>(); private final Set environment = new TreeSet<>(); private final Set devices = new TreeSet<>(); // Misc private final Set capabilities = new TreeSet<>(); private final String registryUrl; private final String restartPolicy; private final boolean hostNetworkingEnabled; private final boolean privilegedMode; public ImageTemplateHolder(final String registryUrl, final String restartPolicy, final boolean hostNetworkingEnabled, final boolean privilegedMode) { this.registryUrl = registryUrl; this.restartPolicy = restartPolicy; this.hostNetworkingEnabled = hostNetworkingEnabled; this.privilegedMode = privilegedMode; } public final boolean hasCapability(final DockerCapability capability) { return capabilities.contains(capability); } public final void addCapability(final DockerCapability capability) { capabilities.add(capability); } public final List getCapabilities() { return new ArrayList<>(capabilities); } public final void addPort(final PortTemplateItem port) { ports.add(port); } public final void addVolume(final VolumeTemplateItem volume) { volumes.add(volume); } public final void addEnvironment(final EnvironmentTemplateItem env) { environment.add(env); } public final void addDevice(final DeviceTemplateItem device) { devices.add(device); } public final List getPorts() { return new ArrayList<>(ports); } public final List getVolumes() { return new ArrayList<>(volumes); } public final List getEnv() { return new ArrayList<>(environment); } public final List getDevices() { return new ArrayList<>(devices); } public final String getRestartPolicy() { return restartPolicy == null ? "no" : restartPolicy; } public final boolean isHostNetworkingEnabled() { return hostNetworkingEnabled; } public final boolean isPrivilegedMode() { return privilegedMode; } public final String getRegistryUrl() { return registryUrl; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/template/PortTemplateItem.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.template; public class PortTemplateItem extends AbstractTemplateItem { private final Protocol protocol; public PortTemplateItem(final Integer port, final String description, final Protocol protocol) { super(port, description); this.protocol = protocol; } public final Integer getPort() { return getName(); } public final String getProtocol() { return getProtocolAsProtocol().protocolName; } public final Protocol getProtocolAsProtocol() { return protocol; } public enum Protocol { Tcp("tcp"), Udp("udp"); private final String protocolName; Protocol(final String protocolName) { this.protocolName = protocolName; } public static Protocol fromName(final String protocolName) { for (Protocol protocol : values()) { if (protocol.protocolName.equals(protocolName)) { return protocol; } } throw new IllegalArgumentException("Unknown protocol " + protocolName); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/template/TemplateItem.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.template; public interface TemplateItem { T getName(); String getDescription(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/types/meta/template/VolumeTemplateItem.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.types.meta.template; public class VolumeTemplateItem extends AbstractTemplateItem { private final boolean readonly; public VolumeTemplateItem(final String volume, final String description, final boolean readonly) { super(volume, description); this.readonly = readonly; } public final String getVolume() { return getName(); } public final boolean isReadonly() { return readonly; } public enum Protocol { Tcp("tcp"), Udp("udp"); private final String protocolName; Protocol(final String protocolName) { this.protocolName = protocolName; } public static Protocol fromName(final String protocolName) { for (Protocol protocol : values()) { if (protocol.protocolName.equals(protocolName)) { return protocol; } } throw new IllegalArgumentException("Unknown protocol " + protocolName); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/ApiException.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web; public class ApiException extends RuntimeException { public ApiException(final String message, final Throwable cause) { super(message, cause); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/AppRole.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web; import io.javalin.core.security.Role; public enum AppRole implements Role { Anyone, Admin } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/LocationUtils.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web; import java.util.stream.Stream; public class LocationUtils { public static boolean isUnAuthenticatedLocation(final String location) { return location.startsWith(Locations.Static.Assets); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/Locations.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web; public interface Locations { interface Static { String Static = "/static"; String Assets = "/assets"; } // Error handling String UnhandledError = "/error"; String MethodNotAllowed = "/error"; // All pages String Home = "/"; String Login = "/login"; String Image = "/image"; interface Api { String Images = "/api/v1/images"; } interface Internal { String Api = "/internalapi"; String Repository = "repository"; String Image = "image"; String Schedule = "schedule"; String Sync = "sync"; String Stats = "stats"; String Track = "track"; String Template = "template"; } interface Admin { String Repositories = "/admin/repositories"; String Images = "/admin/images"; String ImageEdit = "/admin/image"; String Schedules = "/admin/schedules"; String Users = "/admin/users"; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/PageModelAttributes.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web; public interface PageModelAttributes { String AuthenticatedUser = "__AuthenticatedUser"; String ContextAdapter = "__ContextAdapter"; String SystemAlerts = "__SystemAlerts"; } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/PageModelSpec.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web; import java.util.HashMap; import java.util.Map; public class PageModelSpec { private final String viewName; private final Map model; public PageModelSpec(final String viewName) { this.viewName = viewName; this.model = new HashMap<>(); } public final String getViewName() { return viewName; } public final Map getModel() { return model; } public void addModelAttribute(final String key, final Object value) { model.put(key, value); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/SessionAttributes.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web; public interface SessionAttributes { String AuthenticatedUser = "io.linuxserver.fleet.web.SessionAttributes.__AuthenticatedUser"; String ContextAdapter = "io.linuxserver.fleet.web.SessionAttributes.__ContextAdapter"; } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/WebRouteController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web; import io.javalin.Javalin; import io.javalin.core.validation.JavalinValidation; import io.javalin.http.staticfiles.Location; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.core.config.WebConfiguration; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.ImageLookupKey; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.key.UserKey; import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic.StatGroupMode; import io.linuxserver.fleet.v2.web.routes.*; import static io.javalin.apibuilder.ApiBuilder.*; import static io.javalin.core.security.SecurityUtil.roles; public class WebRouteController { private final Javalin webInstance; private final InternalApiController apiController; public WebRouteController(final FleetAppController app) { apiController = new InternalApiController(app); final WebConfiguration webConfiguration = app.getWebConfiguration(); webInstance = Javalin.create(config -> { config.showJavalinBanner = false; config.addStaticFiles(Locations.Static.Static); config.addStaticFiles(app.getAppProperties().getStaticFilesPath().toString(), Location.EXTERNAL); config.accessManager(new DefaultAccessManager()); }).start(webConfiguration.getPort()); Javalin.log.info(printBanner()); JavalinValidation.register(StatGroupMode.class, StatGroupMode::valueOf); JavalinValidation.register(ImageKey.class, ImageKey::parse); JavalinValidation.register(ImageLookupKey.class, ImageLookupKey::new); JavalinValidation.register(RepositoryKey.class, RepositoryKey::parse); JavalinValidation.register(UserKey.class, s -> new UserKey(Integer.parseInt(s))); webInstance.exception(Exception.class, (e, ctx) -> { ctx.status(400); ctx.result(e.getMessage()); }); webInstance.routes(() -> { final LoginController loginController = new LoginController(app); get( Locations.Login, loginController, roles(AppRole.Anyone)); post(Locations.Login, loginController, roles(AppRole.Anyone)); get(Locations.Home, new HomeController( app), roles(AppRole.Anyone)); get(Locations.Image, new ImageController(app), roles(AppRole.Anyone)); get(Locations.Admin.Repositories, new AdminRepositoryController(app), roles(AppRole.Admin)); get(Locations.Admin.Images, new AdminImageController( app), roles(AppRole.Admin)); get(Locations.Admin.Schedules, new AdminScheduleController( app), roles(AppRole.Admin)); final AdminImageEditController imageEditController = new AdminImageEditController(app); get( Locations.Admin.ImageEdit, imageEditController, roles(AppRole.Admin)); post(Locations.Admin.ImageEdit, imageEditController, roles(AppRole.Admin)); final AdminUserController userController = new AdminUserController(app); get( Locations.Admin.Users, userController, roles(AppRole.Admin)); post(Locations.Admin.Users, userController, roles(AppRole.Admin)); path(Locations.Internal.Api, () -> { path(Locations.Internal.Repository, () -> { put( apiController::updateRepositorySpec, roles(AppRole.Admin)); post( apiController::addNewRepository, roles(AppRole.Admin)); delete(apiController::deleteRepository, roles(AppRole.Admin)); path(Locations.Internal.Sync, () -> { put(apiController::syncRepository, roles(AppRole.Admin)); }); }); path(Locations.Internal.Image, () -> { put(apiController::updateImageSpec, roles(AppRole.Admin)); path(Locations.Internal.Sync, () -> { put(apiController::syncImage, roles(AppRole.Admin)); }); path(Locations.Internal.Stats, () -> { get(apiController::getImagePullHistory, roles(AppRole.Anyone)); }); path(Locations.Internal.Track, () -> { put( apiController::trackNewBranch, roles(AppRole.Admin)); delete(apiController::removeTrackedBranch, roles(AppRole.Admin)); }); }); path(Locations.Internal.Schedule, () -> { put(apiController::runSchedule, roles(AppRole.Admin)); }); }); final LegacyExternalApiController externalApiController = new LegacyExternalApiController(app); get(Locations.Api.Images, externalApiController::fetchAllImages, roles(AppRole.Anyone)); }); Runtime.getRuntime().addShutdownHook(new Thread(webInstance::stop)); } private static String printBanner() { return "\n / _| | ___ ___| |_\n" + "| |_| |/ _ \\/ _ | __|\n" + "| _| | __| __| |_\n" + "|_| |_|\\___|\\___|\\__|"; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/freemarker/CustomFreemarkerTemplate.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.freemarker; import freemarker.template.TemplateMethodModelEx; public interface CustomFreemarkerTemplate extends TemplateMethodModelEx { String getName(); } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/freemarker/Java8DateTimeMethod.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.freemarker; import freemarker.ext.beans.StringModel; import freemarker.template.SimpleScalar; import freemarker.template.TemplateModelException; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.List; public class Java8DateTimeMethod implements CustomFreemarkerTemplate { @Override public String getName() { return "formatDate"; } @Override public Object exec(List arguments) throws TemplateModelException { if (arguments.size() != 2) { throw new TemplateModelException("Wrong arguments"); } final TemporalAccessor time = (TemporalAccessor) ((StringModel) arguments.get(0)).getWrappedObject(); final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(((SimpleScalar) arguments.get(1)).getAsString()); return formatter.format(time); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/request/json/NewRepositoryRequest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.request.json; import com.fasterxml.jackson.annotation.JsonProperty; public class NewRepositoryRequest { @JsonProperty private String repositoryName; public final String getRepositoryName() { return repositoryName; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/request/json/UpdateImageSpecRequest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.request.json; import com.fasterxml.jackson.annotation.JsonProperty; public class UpdateImageSpecRequest { @JsonProperty private String imageKey; @JsonProperty private boolean syncEnabled; @JsonProperty private String versionMask; @JsonProperty private boolean stable; @JsonProperty private boolean hidden; @JsonProperty private boolean deprecated; public final String getImageKey() { return imageKey; } public final boolean isSyncEnabled() { return syncEnabled; } public final String getVersionMask() { return versionMask; } public final boolean isStable() { return stable; } public final boolean isHidden() { return hidden; } public boolean isDeprecated() { return deprecated; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/request/json/UpdateRepositoryRequest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.request.json; import com.fasterxml.jackson.annotation.JsonProperty; public class UpdateRepositoryRequest { @JsonProperty private String repositoryKey; @JsonProperty private boolean syncEnabled; @JsonProperty private String versionMask; public final String getRepositoryKey() { return repositoryKey; } public final boolean isSyncEnabled() { return syncEnabled; } public final String getVersionMask() { return versionMask; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/AbstractPageHandler.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.javalin.http.Handler; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.service.AbstractAppService; import io.linuxserver.fleet.v2.web.PageModelAttributes; import io.linuxserver.fleet.v2.web.PageModelSpec; import io.linuxserver.fleet.v2.web.SessionAttributes; import io.linuxserver.fleet.v2.web.freemarker.CustomFreemarkerTemplate; import io.linuxserver.fleet.v2.web.freemarker.Java8DateTimeMethod; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import static io.javalin.plugin.rendering.template.TemplateUtil.model; public abstract class AbstractPageHandler extends AbstractAppService implements Handler { private final Logger LOGGER = LoggerFactory.getLogger(getClass()); private static final List CUSTOM_TEMPLATES; static { CUSTOM_TEMPLATES = new ArrayList<>(); CUSTOM_TEMPLATES.add(new Java8DateTimeMethod()); } AbstractPageHandler(final FleetAppController controller) { super(controller); LOGGER.info("Registering web route."); } @Override public final void handle(@NotNull Context ctx) { try { PageModelSpec spec; if ("get".equalsIgnoreCase(ctx.method())) { spec = handlePageLoad(ctx); } else if ("post".equalsIgnoreCase(ctx.method())) { spec = handleFormSubmission(ctx); } else { ctx.render("views/pages/error.ftl", model("error", "You can't load the page this way.")); return; } injectCustomMethods(spec); injectTopLevelModelAttributes(ctx, spec); checkViewForRedirect(ctx, spec); } catch (Throwable e) { LOGGER.error("Unexpected error occurred when loading page.", e); ctx.render("views/pages/error.ftl", model("error", "Something unexepected happened", "exception", e)); } } protected abstract PageModelSpec handlePageLoad(Context ctx); protected abstract PageModelSpec handleFormSubmission(Context ctx); protected void injectCustomMethods(final PageModelSpec spec) { for (CustomFreemarkerTemplate template : CUSTOM_TEMPLATES) { spec.addModelAttribute(template.getName(), template); } } private void injectTopLevelModelAttributes(final Context ctx, final PageModelSpec spec) { spec.addModelAttribute(PageModelAttributes.AuthenticatedUser, ctx.sessionAttribute(SessionAttributes.AuthenticatedUser)); spec.addModelAttribute(PageModelAttributes.SystemAlerts, getController().getSystemAlerts()); } private void checkViewForRedirect(final Context ctx, final PageModelSpec spec) { if (isRedirect(spec.getViewName())) { ctx.redirect(spec.getViewName().split(":", 2)[1]); } else { ctx.render(spec.getViewName(), spec.getModel()); } } private boolean isRedirect(final String view) { return null != view && view.startsWith("redirect:"); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/AdminImageController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.service.ImageService; import io.linuxserver.fleet.v2.web.PageModelSpec; public class AdminImageController extends AbstractPageHandler { private ImageService imageService; public AdminImageController(final FleetAppController controller) { super(controller); imageService = controller.getImageService(); } @Override protected PageModelSpec handlePageLoad(final Context ctx) { final RepositoryKey repositoryKey = ctx.queryParam("repositoryKey", RepositoryKey.class).getOrNull(); if (null != repositoryKey) { final PageModelSpec modelSpec = new PageModelSpec("views/pages/admin/images.ftl"); modelSpec.addModelAttribute("repository", imageService.getRepository(repositoryKey)); return modelSpec; } else { return new PageModelSpec("views/pages/not-found.ftl"); } } @Override protected PageModelSpec handleFormSubmission(final Context ctx) { return null; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/AdminImageEditController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.javalin.http.UploadedFile; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.service.ImageService; import io.linuxserver.fleet.v2.types.docker.DockerCapability; import io.linuxserver.fleet.v2.types.internal.ImageAppLogo; import io.linuxserver.fleet.v2.types.internal.ImageGeneralInfoUpdateRequest; import io.linuxserver.fleet.v2.types.internal.ImageTemplateRequest; import io.linuxserver.fleet.v2.types.internal.ImageUrlsUpdateRequest; import io.linuxserver.fleet.v2.types.meta.ExternalUrl; import io.linuxserver.fleet.v2.web.PageModelSpec; public class AdminImageEditController extends AbstractPageHandler { private ImageService imageService; public AdminImageEditController(final FleetAppController controller) { super(controller); imageService = controller.getImageService(); } @Override protected PageModelSpec handlePageLoad(final Context ctx) { final String imageKeyParam = ctx.queryParam("imageKey"); if (null != imageKeyParam) { final PageModelSpec modelSpec = new PageModelSpec("views/pages/admin/image-edit.ftl"); modelSpec.addModelAttribute("image", imageService.getImage(ImageKey.parse(imageKeyParam))); modelSpec.addModelAttribute("imageUrlTypes", ExternalUrl.ExternalUrlType.values()); modelSpec.addModelAttribute("containerCapabilities", DockerCapability.values()); return modelSpec; } else { return new PageModelSpec("views/pages/not-found.ftl"); } } @Override protected PageModelSpec handleFormSubmission(final Context ctx) { final String updateType = ctx.formParam("updateType", String.class).get(); final ImageKey imageKey = ctx.formParam("imageKey", ImageKey.class).get(); switch (updateType) { case "GENERAL": handleGeneralUpdate(ctx, imageKey); break; case "TEMPLATE": handleTemplateUpdate(ctx, imageKey); break; case "EXTERNAL_URLS": handleUrlUpdate(ctx, imageKey); break; default: throw new IllegalArgumentException("Unknown updateType provided: " + updateType); } return new PageModelSpec("redirect:/admin/image?imageKey=" + imageKey); } private void handleUrlUpdate(final Context ctx, final ImageKey imageKey) { imageService.updateImageExternalUrls(imageKey, new ImageUrlsUpdateRequest(ctx.formParamMap())); } private void handleGeneralUpdate(final Context ctx, final ImageKey imageKey) { if (!ctx.isMultipartFormData()) { throw new IllegalArgumentException("Form submission must be form/multipart"); } imageService.updateImageGeneralInfo(imageKey, makeInfoRequest(imageKey, ctx)); } private void handleTemplateUpdate(final Context ctx, final ImageKey imageKey) { imageService.updateImageTemplate(imageKey, new ImageTemplateRequest(ctx.formParamMap())); } private ImageGeneralInfoUpdateRequest makeInfoRequest(final ImageKey imageKey, final Context ctx) { return new ImageGeneralInfoUpdateRequest(ctx.formParamMap(), makeImageLogoIfPresent(imageKey, ctx.uploadedFile("ImageAppLogo"))); } private ImageAppLogo makeImageLogoIfPresent(final ImageKey imageKey, final UploadedFile uploadedFile) { if (null != uploadedFile && uploadedFile.getSize() > 0 && uploadedFile.getFilename().length() > 0) { return new ImageAppLogo(imageKey, uploadedFile.getContent(), uploadedFile.getContentType(), uploadedFile.getFilename(), uploadedFile.getSize(), uploadedFile.getExtension()); } return null; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/AdminRepositoryController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.service.ImageService; import io.linuxserver.fleet.v2.web.PageModelSpec; public class AdminRepositoryController extends AbstractPageHandler { private final ImageService imageService; public AdminRepositoryController(final FleetAppController controller) { super(controller); imageService = controller.getImageService(); } @Override protected PageModelSpec handlePageLoad(final Context ctx) { final PageModelSpec modelSpec = new PageModelSpec("views/pages/admin/repositories.ftl"); modelSpec.addModelAttribute("repositories", imageService.getAllRepositories()); return modelSpec; } @Override protected PageModelSpec handleFormSubmission(Context ctx) { return null; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/AdminScheduleController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.service.ScheduleService; import io.linuxserver.fleet.v2.service.SynchronisationService; import io.linuxserver.fleet.v2.web.PageModelSpec; public class AdminScheduleController extends AbstractPageHandler { private final ScheduleService scheduleService; private final SynchronisationService syncService; public AdminScheduleController(final FleetAppController controller) { super(controller); scheduleService = controller.getScheduleService(); syncService = controller.getSynchronisationService(); } @Override protected PageModelSpec handlePageLoad(final Context ctx) { final PageModelSpec modelSpec = new PageModelSpec("views/pages/admin/schedules.ftl"); modelSpec.addModelAttribute("schedules", scheduleService.getLoadedSchedules()); modelSpec.addModelAttribute("queueSize", syncService.getSyncQueue().size()); modelSpec.addModelAttribute("consumerRunning", syncService.isConsumerRunning()); return modelSpec; } @Override protected PageModelSpec handleFormSubmission(Context ctx) { return null; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/AdminUserController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.UserKey; import io.linuxserver.fleet.v2.service.UserService; import io.linuxserver.fleet.v2.types.User; import io.linuxserver.fleet.v2.types.internal.UserOutlineRequest; import io.linuxserver.fleet.v2.web.AppRole; import io.linuxserver.fleet.v2.web.PageModelSpec; public class AdminUserController extends AbstractPageHandler { private UserService userService; public AdminUserController(final FleetAppController controller) { super(controller); userService = controller.getUserService(); } @Override protected PageModelSpec handlePageLoad(final Context ctx) { final PageModelSpec modelSpec = new PageModelSpec("views/pages/admin/users.ftl"); modelSpec.addModelAttribute("users", userService.fetchAllUsers()); return modelSpec; } @Override protected PageModelSpec handleFormSubmission(final Context ctx) { final String action = ctx.queryParam("action", String.class).get(); if ("delete".equalsIgnoreCase(action)) { final UserKey userKey = ctx.formParam("UserPendingDeletion", UserKey.class).get(); final User user = userService.fetchUser(userKey); userService.removeUser(user); } else if("update".equalsIgnoreCase(action)) { final UserKey userKey = ctx.formParam("UserPendingPasswordChange", UserKey.class).get(); final String password = ctx.formParam("UserPassword", String.class).get(); final User user = userService.fetchUser(userKey); if (null == user) { throw new IllegalArgumentException("No user found with key " + userKey); } userService.updateUserPassword(user, password); } else if ("create".equalsIgnoreCase(action)) { final String username = ctx.formParam("NewUserName", String.class).get(); final String password = ctx.formParam("NewUserPassword", String.class).get(); final UserOutlineRequest request = new UserOutlineRequest(username, password, AppRole.Admin); userService.createUserAndHashPassword(request); } return new PageModelSpec("redirect:/admin/users"); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/DefaultAccessManager.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.core.security.AccessManager; import io.javalin.core.security.Role; import io.javalin.http.Context; import io.javalin.http.Handler; import io.linuxserver.fleet.auth.AuthenticatedUser; import io.linuxserver.fleet.v2.web.AppRole; import io.linuxserver.fleet.v2.web.Locations; import io.linuxserver.fleet.v2.web.SessionAttributes; import org.jetbrains.annotations.NotNull; import java.util.Set; public class DefaultAccessManager implements AccessManager { @Override public void manage(@NotNull Handler handler, @NotNull Context ctx, @NotNull Set permittedRoles) throws Exception { if (permittedRoles.contains(AppRole.Anyone)) { handler.handle(ctx); } else { final AuthenticatedUser user = ctx.sessionAttribute(SessionAttributes.AuthenticatedUser); if (null == user) { ctx.redirect(Locations.Login); } else if (isUserRoleValid(user, permittedRoles)){ handler.handle(ctx); } else { ctx.status(401); } } } private boolean isUserRoleValid(final AuthenticatedUser user, final Set permittedRoles) { for (Role role : permittedRoles) { if (user.getRoles().contains(role)) { return true; } } return false; } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/HomeController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.service.ImageService; import io.linuxserver.fleet.v2.types.Repository; import io.linuxserver.fleet.v2.web.PageModelSpec; public class HomeController extends AbstractPageHandler { private final ImageService imageService; public HomeController(final FleetAppController controller) { super(controller); imageService = controller.getImageService(); } @Override protected PageModelSpec handlePageLoad(final Context ctx) { final PageModelSpec modelSpec = new PageModelSpec("views/pages/home.ftl"); final String repositoryLookupParam = ctx.queryParam("key"); if (null == repositoryLookupParam) { final Repository repository = imageService.getFirstRepository(); setSingleRepository(modelSpec, repository); } else { final RepositoryKey repositoryLookupKey = RepositoryKey.parse(repositoryLookupParam); final Repository repository = imageService.getRepository(repositoryLookupKey); setSingleRepository(modelSpec, repository); } modelSpec.addModelAttribute("availableRepositories", imageService.getAllShownRepositories()); return modelSpec; } private void setSingleRepository(PageModelSpec modelSpec, Repository repository) { if (null != repository) { modelSpec.addModelAttribute("selectedRepository", repository); } } @Override protected PageModelSpec handleFormSubmission(final Context ctx) { return new PageModelSpec("views/pages/unsupported.ftl"); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/ImageController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.ImageLookupKey; import io.linuxserver.fleet.v2.service.ImageService; import io.linuxserver.fleet.v2.web.PageModelSpec; public class ImageController extends AbstractPageHandler { private final ImageService imageService; public ImageController(final FleetAppController controller) { super(controller); imageService = controller.getImageService(); } @Override protected PageModelSpec handlePageLoad(final Context ctx) { final String imageLookupParam = ctx.queryParam("name"); if (null != imageLookupParam) { final PageModelSpec modelSpec = new PageModelSpec("views/pages/image.ftl"); modelSpec.addModelAttribute("image", imageService.lookupImage(new ImageLookupKey(imageLookupParam))); return modelSpec; } else { return new PageModelSpec("views/pages/not-found.ftl"); } } @Override protected PageModelSpec handleFormSubmission(final Context ctx) { return new PageModelSpec("views/pages/unsupported.ftl"); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/InternalApiController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.key.ImageKey; import io.linuxserver.fleet.v2.key.RepositoryKey; import io.linuxserver.fleet.v2.key.ScheduleKey; import io.linuxserver.fleet.v2.service.AbstractAppService; import io.linuxserver.fleet.v2.thread.schedule.AppSchedule; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.Repository; import io.linuxserver.fleet.v2.types.api.ApiImagePullHistoryWrapper; import io.linuxserver.fleet.v2.types.api.ApiImageWrapper; import io.linuxserver.fleet.v2.types.api.ApiRepositoryWrapper; import io.linuxserver.fleet.v2.types.api.ApiScheduleWrapper; import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest; import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec; import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic.StatGroupMode; import io.linuxserver.fleet.v2.web.ApiException; import io.linuxserver.fleet.v2.web.request.json.NewRepositoryRequest; import io.linuxserver.fleet.v2.web.request.json.UpdateImageSpecRequest; import io.linuxserver.fleet.v2.web.request.json.UpdateRepositoryRequest; import java.sql.Connection; public class InternalApiController extends AbstractAppService { public InternalApiController(final FleetAppController controller) { super(controller); } public final void updateImageSpec(final Context ctx) { try { final UpdateImageSpecRequest request = ctx.bodyValidator(UpdateImageSpecRequest.class) .check(req -> req.getImageKey() != null).get(); final ItemSyncSpec spec = ItemSyncSpec.Default.copyOf(); spec.setSynchronised(request.isSyncEnabled()); spec.setStable(request.isStable()); spec.setHidden(request.isHidden()); spec.setDeprecated(request.isDeprecated()); spec.setVersionMask(request.getVersionMask()); final Image updated = getController().getImageService() .updateImageSpec(ImageKey.parse(request.getImageKey()), spec); ctx.json(new ApiImageWrapper(updated)); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public final void updateRepositorySpec(final Context ctx) { try { final UpdateRepositoryRequest request = ctx.bodyValidator(UpdateRepositoryRequest.class) .check(req -> req.getRepositoryKey() != null).get(); final ItemSyncSpec spec = ItemSyncSpec.Default.copyOf(); spec.setSynchronised(request.isSyncEnabled()); spec.setVersionMask(request.getVersionMask()); final Repository updated = getController().getImageService() .updateRepositorySpec(RepositoryKey.parse(request.getRepositoryKey()), spec); ctx.json(new ApiRepositoryWrapper(updated)); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public final void addNewRepository(final Context ctx) { try { final NewRepositoryRequest request = ctx.bodyValidator(NewRepositoryRequest.class) .check(req -> req.getRepositoryName() != null).get(); final Repository newlyCreatedRepository = getController() .verifyRepositoryAndCreateOutline(new RepositoryOutlineRequest(request.getRepositoryName())); ctx.json(new ApiRepositoryWrapper(newlyCreatedRepository)); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public final void runSchedule(final Context ctx) { try { final Integer scheduleKey = ctx.formParam("scheduleKey", Integer.class).get(); final AppSchedule schedule = getController().getScheduleService().forceRun(new ScheduleKey(scheduleKey)); ctx.json(new ApiScheduleWrapper(schedule)); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public final void syncRepository(final Context ctx) { try { final RepositoryKey repositoryKey = ctx.formParam("repositoryKey", RepositoryKey.class).get(); final Repository repository = getController().getImageService().getRepository(repositoryKey); getController().synchroniseRepository(repository); ctx.json(new ApiRepositoryWrapper(repository)); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public final void syncImage(final Context ctx) { try { final ImageKey imageKeyParam = ctx.formParam("imageKey", ImageKey.class).get(); getController().synchroniseImage(imageKeyParam); ctx.json("OK"); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public void deleteRepository(final Context ctx) { try { final RepositoryKey repositoryKeyParam = ctx.queryParam("repositoryKey", RepositoryKey.class).get(); getController().getImageService().removeRepository(repositoryKeyParam); ctx.result("OK"); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public void getImagePullHistory(final Context ctx) { try { final ImageKey imageKeyParam = ctx.queryParam("imageKey", ImageKey.class).get(); final StatGroupMode groupMode = ctx.queryParam("groupMode", StatGroupMode.class).get(); final Image cachedImage = getController().getImageService().getImage(imageKeyParam); ctx.json(new ApiImagePullHistoryWrapper(cachedImage.getMetaData().getHistoryFor(groupMode), groupMode)); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public void trackNewBranch(final Context ctx) { try { final ImageKey imageKey = ctx.formParam("imageKey", ImageKey.class).get(); final String branchName = ctx.formParam("branchName", String.class).get(); getController().trackBranch(imageKey, branchName); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } public void removeTrackedBranch(final Context ctx) { try { final ImageKey imageKey = ctx.queryParam("imageKey", ImageKey.class).get(); final String branchName = ctx.queryParam("branchName", String.class).get(); getController().getImageService().removeTrackedBranch(imageKey, branchName); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/LegacyExternalApiController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.service.AbstractAppService; import io.linuxserver.fleet.v2.types.Image; import io.linuxserver.fleet.v2.types.Repository; import io.linuxserver.fleet.v2.types.api.external.AllImagesExternalApiResponse; import io.linuxserver.fleet.v2.types.api.external.ExternalApiImage; import io.linuxserver.fleet.v2.types.api.external.ExternalApiResponse; import io.linuxserver.fleet.v2.types.api.external.templates.*; import io.linuxserver.fleet.v2.types.docker.DockerCapability; import io.linuxserver.fleet.v2.types.meta.template.*; import io.linuxserver.fleet.v2.web.ApiException; import java.util.List; public class LegacyExternalApiController extends AbstractAppService { public LegacyExternalApiController(final FleetAppController controller) { super(controller); } public final void fetchAllImages(final Context ctx) { final boolean verboseOutput = ctx.queryParam("verbose", Boolean.class, "false").get(); try { final AllImagesExternalApiResponse responseData = new AllImagesExternalApiResponse(); final List repositories = getController().getImageService().getAllShownRepositories(); for (Repository repository : repositories) { for (Image image : repository.getImages()) { final ExternalApiImage apiImage = responseData.addImage(image.getRepositoryName(), image.getName(), image.getPullCount(), image.getLatestTag().getVersion(), image.getMetaData().getCategory(), image.isStable(), image.isDeprecated()); if (verboseOutput) { enrichImageWithTemplateData(apiImage, image.getMetaData().getTemplates()); } } } ctx.json(new ExternalApiResponse<>(ExternalApiResponse.ApiStatus.OK, responseData)); } catch (IllegalArgumentException e) { throw new ApiException(e.getMessage(), e); } } private void enrichImageWithTemplateData(final ExternalApiImage apiImage, final ImageTemplateHolder templateHolder) { final ApiTemplateHolder apiTemplateHolder = new ApiTemplateHolder(templateHolder.isHostNetworkingEnabled(), templateHolder.isPrivilegedMode()); for (PortTemplateItem port : templateHolder.getPorts()) { apiTemplateHolder.addPort(new ApiPortTemplate(port.getPort(), port.getProtocol(), port.getDescription())); } for (VolumeTemplateItem volume : templateHolder.getVolumes()) { apiTemplateHolder.addVolume(new ApiVolumeTemplate(volume.getVolume(), volume.isReadonly(), volume.getDescription())); } for (EnvironmentTemplateItem env : templateHolder.getEnv()) { apiTemplateHolder.addEnv(new ApiEnvTemplate(env.getEnv(), env.getExampleValue(), env.getDescription())); } for (DeviceTemplateItem device : templateHolder.getDevices()) { apiTemplateHolder.addDevice(new ApiDeviceTemplate(device.getDevice(), device.getDescription())); } for (DockerCapability capability : templateHolder.getCapabilities()) { apiTemplateHolder.addCapability(capability.name()); } apiImage.setTemplateSpec(apiTemplateHolder); } } ================================================ FILE: src/main/java/io/linuxserver/fleet/v2/web/routes/LoginController.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.auth.AuthenticationResult; import io.linuxserver.fleet.core.FleetAppController; import io.linuxserver.fleet.v2.web.PageModelSpec; import io.linuxserver.fleet.v2.web.SessionAttributes; public class LoginController extends AbstractPageHandler { public LoginController(final FleetAppController controller) { super(controller); } @Override protected PageModelSpec handlePageLoad(final Context ctx) { if (null != ctx.queryParam("invalidate")) { doLogOut(ctx); } return new PageModelSpec("views/pages/login.ftl"); } @Override protected PageModelSpec handleFormSubmission(final Context ctx) { if (null != ctx.formParam("invalidate")) { doLogOut(ctx); } else { final boolean loggedIn = doLogIn(ctx); if (!loggedIn) { final PageModelSpec pageModelSpec = new PageModelSpec("views/pages/login.ftl"); pageModelSpec.addModelAttribute("loginFailed", "Username or password was incorrect."); return pageModelSpec; } } return new PageModelSpec("redirect:/"); } private boolean doLogIn(final Context ctx) { final String username = ctx.formParam("username"); final String password = ctx.formParam("password"); final AuthenticationResult result = getController().authenticateCredentials(username, password); if (result.isAuthenticated()) { ctx.sessionAttribute(SessionAttributes.AuthenticatedUser, result.getUser()); return true; } else { return false; } } private void doLogOut(final Context ctx) { ctx.req.getSession(false).invalidate(); } } ================================================ FILE: src/main/resources/db/migration/V1.0__CreateTables.sql ================================================ CREATE TABLE Repositories ( `id` INT NOT NULL auto_increment PRIMARY KEY, `name` VARCHAR(255) NOT NULL, `version_mask` VARCHAR(255) DEFAULT NULL, `sync_enabled` TINYINT NOT NULL DEFAULT 0, `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY (`name`) ) ENGINE=InnoDB; CREATE TABLE Images ( `id` INT NOT NULL auto_increment PRIMARY KEY, `repository` INT NOT NULL, `name` VARCHAR(255) NOT NULL, `pulls` BIGINT DEFAULT NULL, `latest_version` VARCHAR(100) DEFAULT NULL, `version_mask` VARCHAR(255) DEFAULT NULL, `hidden` TINYINT NOT NULL DEFAULT 0, `unstable` TINYINT NOT NULL DEFAULT 0, `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY (`repository`, `name`), FOREIGN KEY (`repository`) REFERENCES Repositories(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; ================================================ FILE: src/main/resources/db/migration/V1.10__UpdateImageViewWithRepository.sql ================================================ DELIMITER // CREATE OR REPLACE VIEW `Image_View` AS ( SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, repositories.`name` AS `RepositoryName`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `LatestTagVersion`, images.`latest_version_raw` AS `LatestMaskedTagVersion`, images.`latest_version_buildtime` AS `LatestTagBuildDate`, images.`version_mask` AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`deprecated` AS `ImageDeprecated`, images.`deprecation_reason` AS `ImageDeprecationReason`, images.`modified` AS `ModifiedTime` FROM Images images JOIN Repositories repositories ON repositories.`id` = images.`repository` ); // ================================================ FILE: src/main/resources/db/migration/V1.1__CreateSprocs.sql ================================================ DELIMITER // CREATE PROCEDURE `Repository_Save` ( in_id INT, in_name VARCHAR(255), in_version_mask VARCHAR(255), in_sync_enabled TINYINT, OUT out_id INT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF in_id IS NULL THEN INSERT INTO Repositories ( `name`, `version_mask`, `sync_enabled` ) VALUES ( in_name, in_version_mask, in_sync_enabled ); SET out_id = LAST_INSERT_ID(); ELSE UPDATE Repositories SET `name` = in_name, `version_mask` = in_version_mask, `sync_enabled` = in_sync_enabled WHERE `id` = in_id; SET out_id = in_id; END IF; SET out_status = 0; SET out_message = 'OK'; END; // CREATE PROCEDURE `Repository_Get` ( in_id INT ) BEGIN SELECT `id` AS `RepositoryId`, `name` AS `RepositoryName`, `version_mask` AS `RepositoryVersionMask`, `sync_enabled` AS `SyncEnabled`, `modified` AS `ModifiedTime` FROM Repositories WHERE `id` = in_id; END; // CREATE PROCEDURE `Repository_GetByName` ( in_name VARCHAR(255) ) BEGIN SELECT `id` AS `RepositoryId`, `name` AS `RepositoryName`, `version_mask` AS `RepositoryVersionMask`, `sync_enabled` AS `SyncEnabled`, `modified` AS `ModifiedTime` FROM Repositories WHERE `name` = in_name; END; // CREATE PROCEDURE `Repository_Delete` ( in_id INT ) BEGIN DELETE FROM Images WHERE `repository` = in_id; DELETE FROM Repositories WHERE `id` = in_id; END; // CREATE PROCEDURE `Repository_GetAll` () BEGIN SELECT `id` AS `RepositoryId`, `name` AS `RepositoryName`, `version_mask` AS `RepositoryVersionMask`, `sync_enabled` AS `SyncEnabled`, `modified` AS `ModifiedTime` FROM Repositories ORDER BY `name` ASC; END; // CREATE PROCEDURE `Image_Get` ( in_id INT ) BEGIN SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, COALESCE(images.`version_mask`, repos.`version_mask`) AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`modified` AS `ModifiedTime` FROM Images images INNER JOIN Repositories repos ON repos.`id` = images.`repository` WHERE images.`id` = in_id; END; // CREATE PROCEDURE `Image_GetByName` ( in_repo_id INT, in_image_name VARCHAR(255) ) BEGIN SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, COALESCE(images.`version_mask`, repos.`version_mask`) AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`modified` AS `ModifiedTime` FROM Images images INNER JOIN Repositories repos ON repos.`id` = images.`repository` WHERE images.`name` = in_image_name AND repos.`id` = in_repo_id; END; // CREATE PROCEDURE `Image_Save` ( in_id INT, in_repository INT, in_name VARCHAR(255), in_pull_count BIGINT, in_version VARCHAR(100), in_version_mask VARCHAR(255), in_hidden TINYINT, in_unstable TINYINT, OUT out_id INT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF in_id IS NULL THEN INSERT INTO Images ( `repository`, `name`, `pulls`, `latest_version`, `version_mask`, `hidden`, `unstable` ) VALUES ( in_repository, in_name, in_pull_count, in_version, in_version_mask, in_hidden, in_unstable ); SET out_id = LAST_INSERT_ID(); ELSE UPDATE Images SET `name` = in_name, `pulls` = in_pull_count, `latest_version` = in_version, `version_mask` = in_version_mask, `hidden` = in_hidden, `unstable` = in_unstable WHERE `id` = in_id; SET out_id = in_id; END IF; SET out_status = 0; SET out_message = "OK"; END; // CREATE PROCEDURE `Image_GetAll` ( in_repository INT, OUT out_total_count INT ) BEGIN SELECT COUNT(*) INTO out_total_count FROM Images WHERE `repository` = in_repository; SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, COALESCE(images.`version_mask`, repos.`version_mask`) AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`modified` AS `ModifiedTime` FROM Images images INNER JOIN Repositories repos ON repos.`id` = images.`repository` WHERE images.`repository` = in_repository ORDER BY images.`name` ASC; END; // DELIMITER ; ================================================ FILE: src/main/resources/db/migration/V1.2__CreateUserTable.sql ================================================ CREATE TABLE Users ( `id` INT NOT NULL auto_increment PRIMARY KEY, `username` VARCHAR(255) NOT NULL, `password` VARCHAR(255) DEFAULT NULL, `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY (`username`) ) ENGINE=InnoDB; ================================================ FILE: src/main/resources/db/migration/V1.3__CreateUserSprocs.sql ================================================ DELIMITER // CREATE PROCEDURE `User_Save` ( in_id INT, in_username VARCHAR(255), in_password VARCHAR(255), OUT out_id INT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF in_id IS NULL THEN INSERT INTO Users ( `username`, `password` ) VALUES ( in_username, in_password ); SET out_id = LAST_INSERT_ID(); ELSE UPDATE Repositories SET `username` = in_username, `password` = in_password WHERE `id` = in_id; SET out_id = in_id; END IF; SET out_status = 0; SET out_message = 'OK'; END; // CREATE PROCEDURE `User_GetAll` () BEGIN SELECT `id` AS `UserId`, `username` AS `UserName`, `password` AS `UserPassword`, `modified` AS `ModifiedTime` FROM Users; END; // CREATE PROCEDURE `User_Get` ( in_id INT ) BEGIN SELECT `id` AS `UserId`, `username` AS `UserName`, `password` AS `UserPassword`, `modified` AS `ModifiedTime` FROM Users WHERE `id` = in_id; END; // CREATE PROCEDURE `User_GetByName` ( in_username VARCHAR(255) ) BEGIN SELECT `id` AS `UserId`, `username` AS `UserName`, `password` AS `UserPassword`, `modified` AS `ModifiedTime` FROM Users WHERE `username` = in_username; END; // CREATE PROCEDURE `User_Delete` ( in_id INT ) BEGIN DELETE FROM Users WHERE `id` = in_id; END; // DELIMITER ; ================================================ FILE: src/main/resources/db/migration/V1.4__AddDeprecationFields.sql ================================================ -- Specifically for deprecation ALTER TABLE Images ADD COLUMN `deprecated` TINYINT NOT NULL DEFAULT 0, ADD COLUMN `deprecation_reason` VARCHAR(255); ================================================ FILE: src/main/resources/db/migration/V1.5__UpdateImageSprocs.sql ================================================ DELIMITER // DROP PROCEDURE `Image_Get`// CREATE PROCEDURE `Image_Get` ( in_id INT ) BEGIN SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, COALESCE(images.`version_mask`, repos.`version_mask`) AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`deprecated` AS `ImageDeprecated`, images.`deprecation_reason` AS `ImageDeprecationReason`, images.`modified` AS `ModifiedTime` FROM Images images INNER JOIN Repositories repos ON repos.`id` = images.`repository` WHERE images.`id` = in_id; END; // DROP PROCEDURE `Image_GetByName`// CREATE PROCEDURE `Image_GetByName` ( in_repo_id INT, in_image_name VARCHAR(255) ) BEGIN SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, COALESCE(images.`version_mask`, repos.`version_mask`) AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`deprecated` AS `ImageDeprecated`, images.`deprecation_reason` AS `ImageDeprecationReason`, images.`modified` AS `ModifiedTime` FROM Images images INNER JOIN Repositories repos ON repos.`id` = images.`repository` WHERE images.`name` = in_image_name AND repos.`id` = in_repo_id; END; // DROP PROCEDURE `Image_Save`// CREATE PROCEDURE `Image_Save` ( in_id INT, in_repository INT, in_name VARCHAR(255), in_pull_count BIGINT, in_version VARCHAR(100), in_version_mask VARCHAR(255), in_hidden TINYINT, in_unstable TINYINT, in_deprecated TINYINT, in_deprecation_reason VARCHAR(255), OUT out_id INT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF in_id IS NULL THEN INSERT INTO Images ( `repository`, `name`, `pulls`, `latest_version`, `version_mask`, `hidden`, `unstable`, `deprecated`, `deprecation_reason` ) VALUES ( in_repository, in_name, in_pull_count, in_version, in_version_mask, in_hidden, in_unstable, in_deprecated, in_deprecation_reason ); SET out_id = LAST_INSERT_ID(); ELSE UPDATE Images SET `name` = in_name, `pulls` = in_pull_count, `latest_version` = in_version, `version_mask` = in_version_mask, `hidden` = in_hidden, `unstable` = in_unstable, `deprecated` = in_deprecated, `deprecation_reason` = in_deprecation_reason WHERE `id` = in_id; SET out_id = in_id; END IF; SET out_status = 0; SET out_message = "OK"; END; // DROP PROCEDURE `Image_GetAll`// CREATE PROCEDURE `Image_GetAll` ( in_repository INT, OUT out_total_count INT ) BEGIN SELECT COUNT(*) INTO out_total_count FROM Images WHERE `repository` = in_repository; SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, COALESCE(images.`version_mask`, repos.`version_mask`) AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`deprecated` AS `ImageDeprecated`, images.`deprecation_reason` AS `ImageDeprecationReason`, images.`modified` AS `ModifiedTime` FROM Images images INNER JOIN Repositories repos ON repos.`id` = images.`repository` WHERE images.`repository` = in_repository ORDER BY images.`name` ASC; END; // DELIMITER ; ================================================ FILE: src/main/resources/db/migration/V1.6__ExtendVersionColumn.sql ================================================ DELIMITER // ALTER TABLE Images MODIFY `latest_version` VARCHAR(255) DEFAULT NULL// DROP PROCEDURE `Image_Save`// CREATE PROCEDURE `Image_Save` ( in_id INT, in_repository INT, in_name VARCHAR(255), in_pull_count BIGINT, in_version VARCHAR(255), in_version_mask VARCHAR(255), in_hidden TINYINT, in_unstable TINYINT, in_deprecated TINYINT, in_deprecation_reason VARCHAR(255), OUT out_id INT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF in_id IS NULL THEN INSERT INTO Images ( `repository`, `name`, `pulls`, `latest_version`, `version_mask`, `hidden`, `unstable`, `deprecated`, `deprecation_reason` ) VALUES ( in_repository, in_name, in_pull_count, in_version, in_version_mask, in_hidden, in_unstable, in_deprecated, in_deprecation_reason ); SET out_id = LAST_INSERT_ID(); ELSE UPDATE Images SET `name` = in_name, `pulls` = in_pull_count, `latest_version` = in_version, `version_mask` = in_version_mask, `hidden` = in_hidden, `unstable` = in_unstable, `deprecated` = in_deprecated, `deprecation_reason` = in_deprecation_reason WHERE `id` = in_id; SET out_id = in_id; END IF; SET out_status = 0; SET out_message = "OK"; END; // ================================================ FILE: src/main/resources/db/migration/V1.7__RemoveCoalesce.sql ================================================ DELIMITER // DROP PROCEDURE `Image_Get`// CREATE PROCEDURE `Image_Get` ( in_id INT ) BEGIN SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, images.`version_mask` AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`deprecated` AS `ImageDeprecated`, images.`deprecation_reason` AS `ImageDeprecationReason`, images.`modified` AS `ModifiedTime` FROM Images images WHERE images.`id` = in_id; END; // DROP PROCEDURE `Image_GetByName`// CREATE PROCEDURE `Image_GetByName` ( in_repo_id INT, in_image_name VARCHAR(255) ) BEGIN SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, images.`version_mask` AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`deprecated` AS `ImageDeprecated`, images.`deprecation_reason` AS `ImageDeprecationReason`, images.`modified` AS `ModifiedTime` FROM Images images INNER JOIN Repositories repos ON repos.`id` = images.`repository` WHERE images.`name` = in_image_name AND repos.`id` = in_repo_id; END; // DROP PROCEDURE `Image_Save`// CREATE PROCEDURE `Image_Save` ( in_id INT, in_repository INT, in_name VARCHAR(255), in_pull_count BIGINT, in_version VARCHAR(100), in_version_mask VARCHAR(255), in_hidden TINYINT, in_unstable TINYINT, in_deprecated TINYINT, in_deprecation_reason VARCHAR(255), OUT out_id INT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF in_id IS NULL THEN INSERT INTO Images ( `repository`, `name`, `pulls`, `latest_version`, `version_mask`, `hidden`, `unstable`, `deprecated`, `deprecation_reason` ) VALUES ( in_repository, in_name, in_pull_count, in_version, in_version_mask, in_hidden, in_unstable, in_deprecated, in_deprecation_reason ); SET out_id = LAST_INSERT_ID(); ELSE UPDATE Images SET `name` = in_name, `pulls` = in_pull_count, `latest_version` = in_version, `version_mask` = in_version_mask, `hidden` = in_hidden, `unstable` = in_unstable, `deprecated` = in_deprecated, `deprecation_reason` = in_deprecation_reason WHERE `id` = in_id; SET out_id = in_id; END IF; SET out_status = 0; SET out_message = "OK"; END; // DROP PROCEDURE `Image_GetAll`// CREATE PROCEDURE `Image_GetAll` ( in_repository INT, OUT out_total_count INT ) BEGIN SELECT COUNT(*) INTO out_total_count FROM Images WHERE `repository` = in_repository; SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `ImageVersion`, images.`version_mask` AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`deprecated` AS `ImageDeprecated`, images.`deprecation_reason` AS `ImageDeprecationReason`, images.`modified` AS `ModifiedTime` FROM Images images INNER JOIN Repositories repos ON repos.`id` = images.`repository` WHERE images.`repository` = in_repository ORDER BY images.`name` ASC; END; // DELIMITER ; ================================================ FILE: src/main/resources/db/migration/V1.8__PullHistoryAndImageMeta.sql ================================================ DELIMITER // CREATE TABLE ImagePullHistory ( `image_id` INT NOT NULL, `pull_timestamp` BIGINT NOT NULL, `pull_count` BIGINT NOT NULL, PRIMARY KEY (`image_id`, `pull_timestamp`) ) ENGINE=InnoDB; // CREATE PROCEDURE `Image_SavePullHistory` ( in_image_id INT, in_image_pulls BIGINT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF EXISTS(SELECT 1 FROM Images WHERE `id` = in_image_id) THEN INSERT INTO ImagePullHistory ( `image_id`, `pull_timestamp`, `pull_count` ) VALUES ( in_image_id, UNIX_TIMESTAMP(NOW()), in_image_pulls ); END IF; SET out_status = 0; SET out_message = "OK"; END; // CREATE PROCEDURE `Image_GetPullHistory` ( in_image_id INT, in_grouping_mode ENUM('hour', 'day', 'week', 'month', 'year') ) BEGIN IF in_grouping_mode = 'hour' THEN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d%H') AS TimeGroup FROM ImagePullHistory WHERE `image_id` = in_image_id GROUP BY `image_id`, TimeGroup ORDER BY TimeGroup; ELSEIF in_grouping_mode = 'day' THEN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d') AS TimeGroup FROM ImagePullHistory WHERE `image_id` = in_image_id GROUP BY `image_id`, TimeGroup ORDER BY TimeGroup; ELSEIF in_grouping_mode = 'week' THEN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, FROM_UNIXTIME(`pull_timestamp`, '%Y%v') AS TimeGroup FROM ImagePullHistory WHERE `image_id` = in_image_id GROUP BY `image_id`, TimeGroup ORDER BY TimeGroup; ELSEIF in_grouping_mode = 'month' THEN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, FROM_UNIXTIME(`pull_timestamp`, '%Y%m') AS TimeGroup FROM ImagePullHistory WHERE `image_id` = in_image_id GROUP BY `image_id`, TimeGroup ORDER BY TimeGroup; ELSEIF in_grouping_mode = 'year' THEN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, FROM_UNIXTIME(`pull_timestamp`, '%Y') AS TimeGroup FROM ImagePullHistory WHERE `image_id` = in_image_id GROUP BY `image_id`, TimeGroup ORDER BY TimeGroup; END IF; END; // DROP PROCEDURE `Image_Save`// CREATE PROCEDURE `Image_Save` ( in_id INT, in_repository INT, in_name VARCHAR(255), in_pull_count BIGINT, in_version VARCHAR(100), in_version_mask VARCHAR(255), in_hidden TINYINT, in_unstable TINYINT, in_deprecated TINYINT, in_deprecation_reason VARCHAR(255), OUT out_id INT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF in_id IS NULL THEN INSERT INTO Images ( `repository`, `name`, `pulls`, `latest_version`, `version_mask`, `hidden`, `unstable`, `deprecated`, `deprecation_reason` ) VALUES ( in_repository, in_name, in_pull_count, in_version, in_version_mask, in_hidden, in_unstable, in_deprecated, in_deprecation_reason ); SET out_id = LAST_INSERT_ID(); SET out_status = 0; SET out_message = 'OK'; ELSE UPDATE Images SET `name` = in_name, `pulls` = in_pull_count, `latest_version` = in_version, `version_mask` = in_version_mask, `hidden` = in_hidden, `unstable` = in_unstable, `deprecated` = in_deprecated, `deprecation_reason` = in_deprecation_reason WHERE `id` = in_id; SET out_id = in_id; CALL Image_SavePullHistory(out_id, in_pull_count, out_status, out_message); END IF; END; // DROP PROCEDURE `Repository_Delete`// CREATE PROCEDURE `Repository_Delete` ( in_id INT ) BEGIN DELETE FROM ImagePullHistory WHERE `image_id` IN (SELECT `id` FROM Images WHERE `repository` = in_id); DELETE FROM Images WHERE `repository` = in_id; DELETE FROM Repositories WHERE `id` = in_id; END; // CREATE PROCEDURE `Image_Delete` ( in_id INT ) BEGIN DELETE FROM ImagePullHistory WHERE `image_id` = in_id; DELETE FROM Images WHERE `id` = in_id; END; // ================================================ FILE: src/main/resources/db/migration/V1.9__ExtraTagFields.sql ================================================ DELIMITER // ALTER TABLE Images ADD COLUMN `latest_version_raw` VARCHAR(200) DEFAULT NULL, ADD COLUMN `latest_version_buildtime` TIMESTAMP NULL DEFAULT NULL; // CREATE OR REPLACE VIEW `Image_View` AS ( SELECT images.`id` AS `ImageId`, images.`repository` AS `RepositoryId`, images.`name` AS `ImageName`, images.`pulls` AS `ImagePullCount`, images.`latest_version` AS `LatestTagVersion`, images.`latest_version_raw` AS `LatestMaskedTagVersion`, images.`latest_version_buildtime` AS `LatestTagBuildDate`, images.`version_mask` AS `ImageVersionMask`, images.`hidden` AS `ImageHidden`, images.`unstable` AS `ImageUnstable`, images.`deprecated` AS `ImageDeprecated`, images.`deprecation_reason` AS `ImageDeprecationReason`, images.`modified` AS `ModifiedTime` FROM Images images ); // DROP PROCEDURE `Image_Save`// CREATE PROCEDURE `Image_Save` ( in_id INT, in_repository INT, in_name VARCHAR(255), in_pull_count BIGINT, in_version VARCHAR(100), in_version_mask VARCHAR(255), in_hidden TINYINT, in_unstable TINYINT, in_deprecated TINYINT, in_deprecation_reason VARCHAR(255), in_version_raw VARCHAR(200), in_version_buildtime TIMESTAMP, OUT out_id INT, OUT out_status INT, OUT out_message VARCHAR(100) ) BEGIN IF in_id IS NULL THEN INSERT INTO Images ( `repository`, `name`, `pulls`, `latest_version`, `version_mask`, `hidden`, `unstable`, `deprecated`, `deprecation_reason`, `latest_version_raw`, `latest_version_buildtime` ) VALUES ( in_repository, in_name, in_pull_count, in_version, in_version_mask, in_hidden, in_unstable, in_deprecated, in_deprecation_reason, in_version_raw, in_version_buildtime ); SET out_id = LAST_INSERT_ID(); SET out_status = 0; SET out_message = 'OK'; ELSE UPDATE Images SET `name` = in_name, `pulls` = in_pull_count, `latest_version` = in_version, `version_mask` = in_version_mask, `hidden` = in_hidden, `unstable` = in_unstable, `deprecated` = in_deprecated, `deprecation_reason` = in_deprecation_reason, `latest_version_raw` = in_version_raw, `latest_version_buildtime` = in_version_buildtime WHERE `id` = in_id; SET out_id = in_id; CALL Image_SavePullHistory(out_id, in_pull_count, out_status, out_message); END IF; END; // DROP PROCEDURE `Image_GetAll`// CREATE PROCEDURE `Image_GetAll` ( in_repository INT, OUT out_total_count INT ) BEGIN SELECT COUNT(*) INTO out_total_count FROM Images WHERE `repository` = in_repository; SELECT * FROM Image_View images INNER JOIN Repositories repos ON repos.`id` = images.`RepositoryId` WHERE images.`RepositoryId` = in_repository ORDER BY images.`ImageName` ASC; END; // DROP PROCEDURE `Image_Get`// CREATE PROCEDURE `Image_Get` ( in_id INT ) BEGIN SELECT * FROM Image_View images WHERE images.`ImageId` = in_id; END; // DROP PROCEDURE `Image_GetByName`// CREATE PROCEDURE `Image_GetByName` ( in_repo_id INT, in_image_name VARCHAR(255) ) BEGIN SELECT * FROM Image_View images INNER JOIN Repositories repos ON repos.`id` = images.`RepositoryId` WHERE images.`ImageName` = in_image_name AND repos.`id` = in_repo_id; END; // ================================================ FILE: src/main/resources/db/migration/V2.0__CreateV2TablesAndSprocs.sql ================================================ DELIMITER // CREATE TABLE Repository ( `id` INT NOT NULL auto_increment PRIMARY KEY, `name` VARCHAR(255) NOT NULL, `sync_enabled` TINYINT NOT NULL DEFAULT 1, `version_mask` VARCHAR(255) DEFAULT NULL, `hidden` TINYINT NOT NULL DEFAULT 0, `stable` TINYINT NOT NULL DEFAULT 1, `deprecated` TINYINT NOT NULL DEFAULT 0, `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP(), UNIQUE KEY (`name`) ) ENGINE=InnoDB; // CREATE TABLE Image ( `id` INT NOT NULL auto_increment PRIMARY KEY, `repository` INT NOT NULL, `name` VARCHAR(255) NOT NULL, `description` TEXT DEFAULT NULL, `pulls` BIGINT DEFAULT 0, `stars` BIGINT DEFAULT 0, `sync_enabled` TINYINT NOT NULL DEFAULT 1, `version_mask` VARCHAR(255) DEFAULT NULL, `hidden` TINYINT NOT NULL DEFAULT 0, `stable` TINYINT NOT NULL DEFAULT 1, `deprecated` TINYINT NOT NULL DEFAULT 0, `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP(), UNIQUE KEY (`repository`, `name`), FOREIGN KEY (`repository`) REFERENCES Repository(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE TagBranch ( `id` INT NOT NULL auto_increment PRIMARY KEY, `image_id` INT NOT NULL, `name` VARCHAR(255) NOT NULL, `latest_version` VARCHAR(255) NOT NULL DEFAULT 'Unknown', `protected` TINYINT NOT NULL DEFAULT 0, `build_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), UNIQUE KEY (`image_id`, `name`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE TagDigest ( `branch_id` INT NOT NULL, `size` BIGINT NOT NULL, `arch` VARCHAR(100) NOT NULL, `digest` VARCHAR(255) NOT NULL, `variant` VARCHAR(50) DEFAULT CURRENT_TIMESTAMP(), UNIQUE KEY (`branch_id`, `digest`), FOREIGN KEY (`branch_id`) REFERENCES TagBranch(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE Schedule ( `id` INT NOT NULL auto_increment PRIMARY KEY, `name` VARCHAR(100) NOT NULL, `java_class` VARCHAR(255) NOT NULL, `interval` VARCHAR(50) NOT NULL DEFAULT '1:hours', `delay` VARCHAR(50) NOT NULL DEFAULT '0:seconds' ) ENGINE=InnoDB; // CREATE TABLE AppSetting ( `id` INT NOT NULL auto_increment PRIMARY KEY, `name` VARCHAR(100) NOT NULL, `string_val` VARCHAR(255) DEFAULT NULL, `int_val` INT DEFAULT NULL, `double_val` DOUBLE(6,2) DEFAULT NULL, `boolean_val` TINYINT DEFAULT NULL, UNIQUE KEY (`name`) ) ENGINE=InnoDB; // ALTER TABLE Users ADD COLUMN `role` enum('Admin') DEFAULT 'Admin'; // CREATE OR REPLACE VIEW `Image_View` AS ( SELECT -- Key images.`id` AS `ImageId`, images.`name` AS `ImageName`, images.`repository` AS `RepositoryId`, repositories.`name` AS `RepositoryName`, -- Counts images.`pulls` AS `LatestPullCount`, images.`stars` AS `LatestStarCount`, -- Spec images.`sync_enabled` AS `SyncEnabled`, images.`version_mask` AS `VersionMask`, images.`hidden` AS `Hidden`, images.`stable` AS `Stable`, images.`deprecated` AS `Deprecated`, -- General images.`description` AS `Description`, images.`modified` AS `LastUpdated` FROM Image images JOIN Repository repositories ON repositories.`id` = images.`repository` ); // CREATE OR REPLACE VIEW `RepositoryKey_View` AS ( SELECT `id` AS `RepositoryId`, `name` AS `RepositoryName` FROM Repository ); // CREATE OR REPLACE VIEW `Repository_View` AS ( SELECT -- General `id` AS `RepositoryId`, `name` AS `RepositoryName`, -- Spec `sync_enabled` AS `SyncEnabled`, `version_mask` AS `VersionMask`, `hidden` AS `Hidden`, `stable` AS `Stable`, `deprecated` AS `Deprecated`, `modified` AS `LastUpdated` FROM Repository ); // CREATE OR REPLACE VIEW `ImageKey_View` AS ( SELECT image_view.`ImageId`, image_view.`ImageName`, image_view.`RepositoryId`, image_view.`RepositoryName` FROM Image_View image_view ); // CREATE OR REPLACE VIEW `TagBranch_View` AS ( SELECT `image_id` AS `ImageId`, `id` AS `BranchId`, `name` AS `BranchName`, `latest_version` AS `TagVersion`, `protected` AS `BranchProtected`, `build_date` AS `TagBuildDate` FROM TagBranch ); // CREATE OR REPLACE VIEW `TagDigest_View` AS ( SELECT tag_branch.`image_id` AS `ImageId`, tag_branch.`id` AS `BranchId`, tag_digest.`size` AS `DigestSize`, tag_digest.`digest` AS `DigestSha`, tag_digest.`arch` AS `DigestArch`, tag_digest.`variant` AS `DigestVariant` FROM TagDigest tag_digest JOIN TagBranch tag_branch ON tag_branch.`id` = tag_digest.`branch_id` ); // CREATE OR REPLACE PROCEDURE `Repository_Get` ( in_id INT ) BEGIN SELECT * FROM Repository_View WHERE `RepositoryId` = in_id; END // CREATE OR REPLACE PROCEDURE `Repository_GetRepositoryKeys` () BEGIN SELECT * FROM RepositoryKey_View; END // CREATE OR REPLACE PROCEDURE `Repository_GetImageKeys` ( in_id INT ) BEGIN SELECT * FROM ImageKey_View WHERE `RepositoryId` = in_id; END // CREATE OR REPLACE PROCEDURE `Image_GetTagBranches` ( in_image_id INT ) BEGIN -- Top level branch info SELECT * FROM TagBranch_View WHERE `ImageId` = in_image_id; END; // CREATE OR REPLACE PROCEDURE `Image_GetTagDigests` ( in_branch_id INT ) BEGIN SELECT * FROM TagDigest_View tag_digest WHERE tag_digest.`BranchId` = in_branch_id; END // CREATE OR REPLACE PROCEDURE `Image_CreateTagBranchOutline` ( in_image_id INT, in_branch_name VARCHAR(255) ) BEGIN INSERT INTO TagBranch (`image_id`, `name`) VALUES ( in_image_id, in_branch_name ); -- Top level branch info SELECT * FROM TagBranch_View WHERE `BranchId` = LAST_INSERT_ID(); END; // CREATE OR REPLACE PROCEDURE `Image_StoreTagBranch` ( in_image_id INT, in_branch_id INT, in_latest_version VARCHAR(255), in_build_date TIMESTAMP ) BEGIN UPDATE TagBranch SET `latest_version` = in_latest_version, `build_date` = in_build_date WHERE `id` = in_branch_id AND `image_id` = in_image_id; -- Updating a tag branch should come hand-in-hand with updating digests. DELETE FROM TagDigest WHERE `branch_id` = in_branch_id; END; // CREATE OR REPLACE PROCEDURE `Image_StoreTagDigest` ( in_branch_id INT, in_size BIGINT, in_digest VARCHAR(255), in_arch VARCHAR(255), in_variant VARCHAR(100) ) BEGIN INSERT INTO TagDigest (`branch_id`, `size`, `digest`, `arch`, `variant`) VALUES ( in_branch_id, in_size, in_digest, in_arch, in_variant ); END; // CREATE OR REPLACE PROCEDURE `Image_Get` ( in_id INT ) BEGIN SELECT * FROM Image_View image_view WHERE image_view.`ImageId` = in_id; END; // CREATE OR REPLACE PROCEDURE `Image_Delete` ( in_id INT, OUT out_status enum('Updated', 'NoChange') ) BEGIN IF EXISTS(SELECT `id` FROM Image WHERE `id` = in_id) THEN DELETE FROM Image WHERE `id` = in_id; SET out_status = 'Updated'; ELSE SET out_status = 'NoChange'; END IF; END // CREATE OR REPLACE PROCEDURE `Repository_Delete` ( in_id INT, OUT out_status enum('Updated', 'NoChange') ) BEGIN IF EXISTS(SELECT `id` FROM Repository WHERE `id` = in_id) THEN DELETE FROM Repository WHERE `id` = in_id; SET out_status = 'Updated'; ELSE SET out_status = 'NoChange'; END IF; END // CREATE OR REPLACE PROCEDURE `Repository_Store` ( in_id INT, in_synchronised TINYINT, in_version_mask VARCHAR(255), OUT out_status enum('NoChange', 'Updated') ) BEGIN IF NOT(EXISTS(SELECT `id` FROM Repository WHERE `id` = in_id)) THEN SET out_status = 'NoChange'; ELSE UPDATE Repository SET `sync_enabled` = in_synchronised, `version_mask` = in_version_mask WHERE `id` = in_id; SET out_status = 'Updated'; END IF; END // CREATE OR REPLACE PROCEDURE `Repository_CreateOutline` ( in_name VARCHAR(255), in_modified TIMESTAMP, in_deprecated TINYINT, in_hidden TINYINT, in_stable TINYINT, in_synchronised TINYINT, in_version_mask VARCHAR(255), OUT out_status enum('Inserted', 'Exists') ) BEGIN IF EXISTS(SELECT `id` FROM Repository WHERE `name` = in_name) THEN SET out_status = 'Exists'; ELSE INSERT INTO Repository (`name`, `modified`, `deprecated`, `hidden`, `stable`, `sync_enabled`, `version_mask`) VALUES ( in_name, in_modified, in_deprecated, in_hidden, in_stable, in_synchronised, in_version_mask ); SET out_status = 'Inserted'; SELECT * FROM RepositoryKey_View WHERE RepositoryId = LAST_INSERT_ID(); END IF; END // CREATE OR REPLACE PROCEDURE `Image_StorePullHistory` ( in_image_id INT, in_image_pulls BIGINT, OUT out_status ENUM('Updated', 'NoChange') ) BEGIN IF EXISTS(SELECT 1 FROM Image WHERE `id` = in_image_id) THEN INSERT INTO ImagePullHistory ( `image_id`, `pull_timestamp`, `pull_count` ) VALUES ( in_image_id, UNIX_TIMESTAMP(NOW()), in_image_pulls ); SET out_status = 'Updated'; ELSE SET out_status = 'NoChange'; END IF; END; // CREATE OR REPLACE PROCEDURE `Image_GetStats` ( in_image_id INT ) BEGIN SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d') AS TimeGroup, 'Week' AS GroupMode FROM ImagePullHistory WHERE `image_id` = in_image_id AND `pull_timestamp` > unix_timestamp(now() - interval 7 day) GROUP BY TimeGroup UNION ALL SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d') AS TimeGroup, 'Month' AS GroupMode FROM ImagePullHistory WHERE `image_id` = in_image_id AND `pull_timestamp` > unix_timestamp(now() - interval 1 month) GROUP BY TimeGroup UNION ALL SELECT `image_id` AS ImageId, MAX(`pull_count`) AS ImagePulls, FROM_UNIXTIME(`pull_timestamp`, '%Y%m%d%h') AS TimeGroup, 'Day' AS GroupMode FROM ImagePullHistory WHERE `image_id` = in_image_id AND `pull_timestamp` > unix_timestamp(date(now())) ORDER BY GroupMode, TimeGroup; END; // CREATE OR REPLACE PROCEDURE `Image_Store` ( in_id INT, in_pulls BIGINT, in_stars INT, in_description TEXT, in_modified TIMESTAMP, in_deprecated TINYINT, in_hidden TINYINT, in_stable TINYINT, in_synchronised TINYINT, in_version_mask VARCHAR(255), OUT out_status enum('Updated', 'NoChange') ) BEGIN IF NOT EXISTS(SELECT `id` FROM Image WHERE `id` = in_id) THEN SET out_status = 'NoChange'; ELSE UPDATE Image SET `pulls` = in_pulls, `stars` = in_stars, `description` = in_description, `modified` = in_modified, `deprecated` = in_deprecated, `hidden` = in_hidden, `stable` = in_stable, `sync_enabled` = in_synchronised, `version_mask` = in_version_mask WHERE `id` = in_id; IF ROW_COUNT() <> 1 THEN SET out_status = 'NoChange'; ELSE SET out_status = 'Updated'; END IF; CALL Image_StorePullHistory(in_id, in_pulls, out_status); SELECT * FROM ImageKey_View WHERE `ImageId` = in_id; END IF; END // CREATE OR REPLACE PROCEDURE `Image_CreateOutline` ( in_repository INT, in_name VARCHAR(255), in_description TEXT, in_modified TIMESTAMP, in_deprecated TINYINT, in_hidden TINYINT, in_stable TINYINT, in_synchronised TINYINT, in_version_mask VARCHAR(255), OUT out_status enum('Inserted', 'Exists') ) BEGIN DECLARE var_imageId INT; IF EXISTS(SELECT `id` FROM Image WHERE `repository` = in_repository AND `name` = in_name) THEN SET out_status = 'Exists'; ELSE INSERT INTO Image (`repository`, `name`, `description`, `modified`, `deprecated`, `hidden`, `stable`, `sync_enabled`, `version_mask`) VALUES ( in_repository, in_name, in_description, in_modified, in_deprecated, in_hidden, in_stable, in_synchronised, in_version_mask ); SET var_imageId = LAST_INSERT_ID(); INSERT INTO TagBranch (`image_id`, `name`, `protected`) VALUES ( var_imageId, 'latest', 1 ); SET out_status = 'Inserted'; SELECT * FROM ImageKey_View image_key WHERE image_key.`ImageId` = var_imageId; END IF; END; // CREATE OR REPLACE PROCEDURE `Schedule_GetSpecs` () BEGIN SELECT `id` AS `ScheduleId`, `name` AS `ScheduleName`, `interval` AS `ScheduleInterval`, `delay` AS `ScheduleDelayOffset`, `java_class` AS `ScheduleClass` FROM Schedule; END; // CREATE OR REPLACE VIEW `User_View` AS ( SELECT `id` AS `UserId`, `username` AS `UserName`, `password` AS `UserPassword`, `role` AS `UserRole`, `modified` AS `ModifiedTime` FROM Users ); // CREATE OR REPLACE PROCEDURE `User_Save` ( in_id INT, in_username VARCHAR(255), in_password VARCHAR(255), in_role ENUM('Admin'), OUT out_status ENUM('Exists', 'Inserted') ) BEGIN IF in_id IS NULL THEN INSERT INTO Users ( `username`, `password`, `role` ) VALUES ( in_username, in_password, in_role ); SET out_status = 'Inserted'; SELECT * FROM User_View WHERE `id` = LAST_INSERT_ID(); ELSE SET out_status = 'Exists'; END IF; END; // CREATE OR REPLACE PROCEDURE `User_GetAll` () BEGIN SELECT * FROM User_View; END; // CREATE OR REPLACE PROCEDURE `User_Get` ( in_id INT ) BEGIN SELECT * FROM User_View WHERE UserId = in_id; END; // CREATE OR REPLACE PROCEDURE `User_GetByName` ( in_username VARCHAR(255) ) BEGIN SELECT * FROM User_View WHERE UserName = in_username; END; // CREATE OR REPLACE PROCEDURE `User_Delete` ( in_id INT ) BEGIN DELETE FROM Users WHERE `id` = in_id; END; // ================================================ FILE: src/main/resources/db/migration/V2.1__MigrateToNewTables.sql ================================================ INSERT INTO Repository (`id`, `name`, `sync_enabled`, `version_mask`, `hidden`, `stable`, `deprecated`, `modified`) SELECT `id`, `name`, `sync_enabled`, `version_mask`, 0, 1, 0, `modified` FROM Repositories; INSERT INTO Image (`id`, `repository`, `name`, `description`, `pulls`, `stars`, `sync_enabled`, `version_mask`, `hidden`, `stable`, `deprecated`, `modified`) SELECT `id`, `repository`, `name`, null, `pulls`, 0, 1, `version_mask`, `hidden`, NOT(`unstable`), `deprecated`, `modified` FROM Images; INSERT INTO TagBranch (`image_id`, `name`, `latest_version`, `protected`, `build_date`) SELECT `id`, 'latest', `latest_version`, 1, `latest_version_buildtime` FROM Images; INSERT INTO Schedule (`name`, `interval`, `delay`, `java_class`) VALUES ('SyncAllCachedImages', '1:hours', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.sync.AllImagesSyncSchedule'), ('GetMissingImages', '30:minutes', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.sync.GetMissingImagesSchedule'), ('RefreshCache', '1:days', '15:minutes', 'io.linuxserver.fleet.v2.thread.schedule.cache.RefreshCacheSchedule'), ('TidyHistoricData', '1:days', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.TidyHistoricDataSchedule'), ('CheckAppVersion', '1:days', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.CheckAppVersionSchedule'); ================================================ FILE: src/main/resources/db/migration/V2.2__MetaDataTables.sql ================================================ DELIMITER // CREATE TABLE ImageMetadata ( `image_id` INT NOT NULL, `beta` BIT NOT NULL DEFAULT 0, `category` VARCHAR(255) DEFAULT NULL, `changes` VARCHAR(1000) DEFAULT NULL, `support` VARCHAR(500) DEFAULT NULL, `app_url` VARCHAR(500) DEFAULT NULL, `description` VARCHAR(1000) DEFAULT NULL, `base_image` VARCHAR(255) DEFAULT NULL, `icon_url` VARCHAR(1000) DEFAULT NULL, PRIMARY KEY (`image_id`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE ImageTemplateBase ( `image_id` INT NOT NULL, `url` VARCHAR(255) DEFAULT NULL, `restart` ENUM('no', 'always', 'unless-stopped', 'on-failure') NOT NULL, `host_network` BIT DEFAULT 0, `privileged` BIT DEFAULT 0, PRIMARY KEY (`image_id`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE ImageTemplateEnvironment ( `image_id` INT NOT NULL, `env_key` VARCHAR(100) NOT NULL, `description` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`image_id`, `env_key`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE ImageTemplateExtra ( `image_id` INT NOT NULL, `extra_key` VARCHAR(100) NOT NULL, `description` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`image_id`, `extra_key`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE ImageTemplateDevices ( `image_id` INT NOT NULL, `device` VARCHAR(100) NOT NULL, `description` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`image_id`, `device`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE ImageTemplateVolumes ( `image_id` INT NOT NULL, `volume` VARCHAR(255) NOT NULL, `description` VARCHAR(255) DEFAULT NULL, `read_only` BIT DEFAULT 0, PRIMARY KEY (`image_id`, `volume`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE TABLE ImageTemplatePorts ( `image_id` INT NOT NULL, `port` INT NOT NULL, `description` VARCHAR(255) DEFAULT NULL, `protocol` ENUM('tcp', 'udp') DEFAULT 'tcp', PRIMARY KEY (`image_id`, `port`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE OR REPLACE PROCEDURE `Image_StoreTemplateBase` ( in_image_id INT, in_url VARCHAR(255), in_restart_policy ENUM('no', 'always', 'unless-stopped', 'on-failure'), in_host_network BIT, in_privileged_mode BIT, OUT out_status ENUM('NoChange', 'Updated', 'Inserted') ) BEGIN DECLARE var_rows_affected INT; IF EXISTS(SELECT 1 FROM `ImageTemplateBase` WHERE `image_id` = in_image_id) THEN UPDATE `ImageTemplateBase` SET `url` = in_url, `restart` = in_restart_policy, `host_network` = in_host_network, `privileged` = in_privileged_mode WHERE `image_id` = in_image_id; SET var_rows_affected = ROW_COUNT(); IF ROW_COUNT() = 0 THEN SET out_status = 'NoChange'; ELSE SET out_status = 'Updated'; END IF; ELSE INSERT INTO `ImageTemplateBase` ( `image_id`, `url`, `restart`, `host_network`, `privileged` ) VALUES ( in_image_id, in_url, in_restart_policy, in_host_network, in_privileged_mode ); SET out_status = 'Inserted'; END IF; END // CREATE OR REPLACE PROCEDURE `Image_ClearTemplateData` ( in_id INT, in_current_volumes TEXT, in_current_ports TEXT, in_current_env TEXT, in_current_caps TEXT, in_current_devices TEXT ) BEGIN -- Deletes only orphans. Deletion will not reclaim space, so this should only remove items whenever -- one of them has legimitately been removed, rather than clearing and reinserting each time. DELETE FROM ImageTemplateVolumes WHERE `image_id` = in_id AND NOT FIND_IN_SET(`volume`, in_current_volumes); DELETE FROM ImageTemplatePorts WHERE `image_id` = in_id AND NOT FIND_IN_SET(`port`, in_current_ports); DELETE FROM ImageTemplateEnvironment WHERE `image_id` = in_id AND NOT FIND_IN_SET(`env_key`, in_current_env); DELETE FROM ImageTemplateExtra WHERE `image_id` = in_id AND NOT FIND_IN_SET(`extra_key`, in_current_caps); DELETE FROM ImageTemplateDevices WHERE `image_id` = in_id AND NOT FIND_IN_SET(`device`, in_current_devices); END; // CREATE OR REPLACE PROCEDURE `Image_StoreTemplateVolume` ( in_image_id INT, in_volume_name VARCHAR(255), in_volume_desc VARCHAR(255), in_volume_ro BIT ) BEGIN IF EXISTS(SELECT 1 FROM `ImageTemplateVolumes` WHERE `image_id` = in_image_id AND `volume` = in_volume_name) THEN UPDATE `ImageTemplateVolumes` SET `description` = in_volume_desc, `read_only` = in_volume_ro WHERE `image_id` = in_image_id AND `volume` = in_volume_name; ELSE INSERT INTO `ImageTemplateVolumes` ( `image_id`, `volume`, `description`, `read_only` ) VALUES ( in_image_id, in_volume_name, in_volume_desc, in_volume_ro ); END IF; END // CREATE OR REPLACE PROCEDURE `Image_StoreTemplatePort` ( in_image_id INT, in_port_name INT, in_port_desc VARCHAR(255), in_port_proto ENUM('tcp', 'udp') ) BEGIN IF EXISTS(SELECT 1 FROM `ImageTemplatePorts` WHERE `image_id` = in_image_id AND `port` = in_port_name) THEN UPDATE `ImageTemplatePorts` SET `description` = in_port_desc, `protocol` = in_port_proto WHERE `image_id` = in_image_id AND `port` = in_port_name; ELSE INSERT INTO `ImageTemplatePorts` ( `image_id`, `port`, `description`, `protocol` ) VALUES ( in_image_id, in_port_name, in_port_desc, in_port_proto ); END IF; END // CREATE OR REPLACE PROCEDURE `Image_StoreTemplateEnv` ( in_image_id INT, in_env_key VARCHAR(100), in_env_desc VARCHAR(255) ) BEGIN IF EXISTS(SELECT 1 FROM `ImageTemplateEnvironment` WHERE `image_id` = in_image_id AND `env_key` = in_env_key) THEN UPDATE `ImageTemplateEnvironment` SET `description` = in_env_desc WHERE `image_id` = in_image_id AND `env_key` = in_env_key; ELSE INSERT INTO `ImageTemplateEnvironment` ( `image_id`, `env_key`, `description` ) VALUES ( in_image_id, in_env_key, in_env_desc ); END IF; END // CREATE OR REPLACE PROCEDURE `Image_StoreTemplateDevice` ( in_image_id INT, in_device_name VARCHAR(100), in_device_desc VARCHAR(255) ) BEGIN IF EXISTS(SELECT 1 FROM `ImageTemplateDevices` WHERE `image_id` = in_image_id AND `device` = in_device_name) THEN UPDATE `ImageTemplateDevices` SET `description` = in_device_desc WHERE `image_id` = in_image_id AND `device` = in_device_name; ELSE INSERT INTO `ImageTemplateDevices` ( `image_id`, `device`, `description` ) VALUES ( in_image_id, in_device_name, in_device_desc ); END IF; END // CREATE OR REPLACE PROCEDURE `Image_StoreTemplateExtra` ( in_image_id INT, in_extra_key VARCHAR(100), in_extra_desc VARCHAR(255) ) BEGIN IF EXISTS(SELECT 1 FROM `ImageTemplateExtra` WHERE `image_id` = in_image_id AND `extra_key` = in_extra_key) THEN UPDATE `ImageTemplateExtra` SET `description` = in_extra_desc WHERE `image_id` = in_image_id AND `extra_key` = in_extra_key; ELSE INSERT INTO `ImageTemplateExtra` ( `image_id`, `extra_key`, `description` ) VALUES ( in_image_id, in_extra_key, in_extra_desc ); END IF; END // CREATE OR REPLACE PROCEDURE `Image_GetTemplateBase` ( in_image_id INT ) BEGIN -- BASE SELECT `url` AS `RepositoryUrl`, `restart` AS `RestartPolicy`, `host_network` AS `HostNetworkEnabled`, `privileged` AS `PrivilegedMode` FROM `ImageTemplateBase` WHERE `image_id` = in_image_id; END // CREATE OR REPLACE PROCEDURE `Image_GetTemplates` ( in_image_id INT ) BEGIN SELECT 'Port' AS `ItemType`, `port` AS `ItemName`, `description` AS `ItemDescription`, `protocol` AS `ItemSecondary` FROM `ImageTemplatePorts` WHERE `image_id` = in_image_id UNION ALL SELECT 'Volume' AS `ItemType`, `volume` AS `ItemName`, `description` AS `ItemDescription`, `read_only` AS `ItemSecondary` FROM `ImageTemplateVolumes` WHERE `image_id` = in_image_id UNION ALL SELECT 'Env' AS `ItemType`, `env_key` AS `ItemName`, `description` AS `ItemDescription`, NULL AS `ItemSecondary` FROM `ImageTemplateEnvironment` WHERE `image_id` = in_image_id UNION ALL SELECT 'Device' AS `ItemType`, `device` AS `ItemName`, `description` AS `ItemDescription`, NULL AS `ItemSecondary` FROM `ImageTemplateDevices` WHERE `image_id` = in_image_id UNION ALL SELECT 'Extra' AS `ItemType`, `extra_key` AS `ItemName`, `description` AS `ItemDescription`, NULL AS `ItemSecondary` FROM `ImageTemplateExtra` WHERE `image_id` = in_image_id; END // ================================================ FILE: src/main/resources/db/migration/V2.3__UpdateImageViewForCoreMeta.sql ================================================ DELIMITER // CREATE OR REPLACE VIEW `Image_View` AS ( SELECT -- Key images.`id` AS `ImageId`, images.`name` AS `ImageName`, images.`repository` AS `RepositoryId`, repositories.`name` AS `RepositoryName`, -- Counts images.`pulls` AS `LatestPullCount`, images.`stars` AS `LatestStarCount`, -- Spec images.`sync_enabled` AS `SyncEnabled`, images.`version_mask` AS `VersionMask`, images.`hidden` AS `Hidden`, images.`stable` AS `Stable`, images.`deprecated` AS `Deprecated`, -- General images.`description` AS `Description`, images.`modified` AS `LastUpdated`, -- Core Meta meta.icon_url AS `CoreMetaImagePath`, meta.base_image AS `CoreMetaBaseImage`, meta.category AS `CoreMetaCategory`, meta.support AS `CoreMetaSupportUrl`, meta.app_url AS `CoreMetaAppUrl` FROM Image images JOIN Repository repositories ON repositories.`id` = images.`repository` LEFT JOIN ImageMetadata meta on meta.`image_id` = images.`id` ); // CREATE OR REPLACE PROCEDURE `Image_Store` ( in_id INT, in_pulls BIGINT, in_stars INT, in_description TEXT, in_modified TIMESTAMP, in_deprecated TINYINT, in_hidden TINYINT, in_stable TINYINT, in_synchronised TINYINT, in_version_mask VARCHAR(255), in_category VARCHAR(255), in_support VARCHAR(500), in_app_url VARCHAR(500), in_base_image VARCHAR(255), in_icon_url VARCHAR(1000), OUT out_status enum('Updated', 'NoChange') ) BEGIN IF NOT EXISTS(SELECT `id` FROM Image WHERE `id` = in_id) THEN SET out_status = 'NoChange'; ELSE UPDATE Image SET `pulls` = in_pulls, `stars` = in_stars, `description` = in_description, `modified` = in_modified, `deprecated` = in_deprecated, `hidden` = in_hidden, `stable` = in_stable, `sync_enabled` = in_synchronised, `version_mask` = in_version_mask WHERE `id` = in_id; -- Only add core metadata if it has been provided with at least one value IF ( in_category IS NOT NULL OR in_support IS NOT NULL OR in_app_url IS NOT NULL OR in_base_image IS NOT NULL OR in_icon_url IS NOT NULL ) THEN IF NOT EXISTS(SELECT 1 FROM ImageMetadata WHERE `image_id` = in_id) THEN INSERT INTO ImageMetadata ( `image_id`, `category`, `support`, `app_url`, `base_image`, `icon_url` ) VALUES ( in_id, in_category, in_support, in_app_url, in_base_image, in_icon_url ); ELSE UPDATE ImageMetadata SET `category` = in_category, `support` = in_support, `app_url` = in_app_url, `base_image` = in_base_image, `icon_url` = in_icon_url WHERE `image_id` = in_id; END IF; END IF; IF ROW_COUNT() <> 1 THEN SET out_status = 'NoChange'; ELSE SET out_status = 'Updated'; END IF; CALL Image_StorePullHistory(in_id, in_pulls, out_status); SELECT * FROM ImageKey_View WHERE `ImageId` = in_id; END IF; END // ================================================ FILE: src/main/resources/db/migration/V2.4__UpdateUserSprocs.sql ================================================ DELIMITER // CREATE OR REPLACE VIEW `User_View` AS ( SELECT `id` AS `UserId`, `username` AS `Username`, `password` AS `UserPassword`, `modified` AS `ModifiedTime`, `role` AS `UserRole` FROM Users ); // CREATE OR REPLACE PROCEDURE `User_CreateOutline` ( in_username VARCHAR(255), in_password VARCHAR(255), in_role ENUM('Admin'), OUT out_status ENUM('Inserted', 'NoChange', 'Exists') ) BEGIN IF NOT EXISTS(SELECT 1 FROM User_View WHERE `Username` = in_username) THEN INSERT INTO Users ( `username`, `password`, `role` ) VALUES ( in_username, in_password, in_role ); SET out_status = 'Inserted'; SELECT * FROM User_View WHERE `UserId` = LAST_INSERT_ID(); ELSE SET out_status = 'Exists'; END IF; END; // CREATE OR REPLACE PROCEDURE `User_Save` ( in_id INT, in_username VARCHAR(255), in_password VARCHAR(255), in_role ENUM('Admin'), OUT out_status ENUM('Updated', 'NoChange') ) BEGIN IF EXISTS(SELECT 1 FROM User_View WHERE `Username` = in_username) THEN UPDATE Users SET `password` = in_password, `role` = in_role WHERE `id` = in_id; SET out_status = 'Updated'; SELECT * FROM User_View WHERE `UserId` = in_id; ELSE SET out_status = 'NoChange'; END IF; END; // CREATE OR REPLACE PROCEDURE `User_GetAll` () BEGIN SELECT * FROM User_View; END; // CREATE OR REPLACE PROCEDURE `User_Get` ( in_id INT ) BEGIN SELECT * FROM User_View WHERE `UserId` = in_id; END; // CREATE OR REPLACE PROCEDURE `User_GetByName` ( in_username VARCHAR(255) ) BEGIN SELECT * FROM User_View WHERE `Username` = in_username; END; // CREATE OR REPLACE PROCEDURE `User_Delete` ( in_id INT ) BEGIN DELETE FROM Users WHERE `id` = in_id; END; // DELIMITER ; ================================================ FILE: src/main/resources/db/migration/V2.5__BranchRemovalSproc.sql ================================================ DELIMITER // CREATE OR REPLACE PROCEDURE `Image_RemoveOrphanBranches` ( in_image_id VARCHAR(255), in_branchIds VARCHAR(255) ) BEGIN DELETE FROM TagBranch WHERE `image_id` = in_image_id AND NOT FIND_IN_SET(`id`, in_branchIds); END; // CREATE OR REPLACE VIEW `Image_View` AS ( SELECT -- Key images.`id` AS `ImageId`, images.`name` AS `ImageName`, images.`repository` AS `RepositoryId`, repositories.`name` AS `RepositoryName`, -- Counts images.`pulls` AS `LatestPullCount`, images.`stars` AS `LatestStarCount`, -- Spec images.`sync_enabled` AS `SyncEnabled`, images.`version_mask` AS `VersionMask`, images.`hidden` AS `Hidden`, images.`stable` AS `Stable`, images.`deprecated` AS `Deprecated`, -- General images.`description` AS `Description`, images.`modified` AS `LastUpdated`, -- Core Meta meta.icon_url AS `CoreMetaImagePath`, meta.base_image AS `CoreMetaBaseImage`, meta.category AS `CoreMetaCategory` FROM Image images JOIN Repository repositories ON repositories.`id` = images.`repository` LEFT JOIN ImageMetadata meta on meta.`image_id` = images.`id` ); // CREATE OR REPLACE PROCEDURE `Image_Store` ( in_id INT, in_pulls BIGINT, in_stars INT, in_description TEXT, in_modified TIMESTAMP, in_deprecated TINYINT, in_hidden TINYINT, in_stable TINYINT, in_synchronised TINYINT, in_version_mask VARCHAR(255), OUT out_status enum('Updated', 'NoChange') ) BEGIN IF NOT EXISTS(SELECT `id` FROM Image WHERE `id` = in_id) THEN SET out_status = 'NoChange'; ELSE UPDATE Image SET `pulls` = in_pulls, `stars` = in_stars, `description` = in_description, `modified` = in_modified, `deprecated` = in_deprecated, `hidden` = in_hidden, `stable` = in_stable, `sync_enabled` = in_synchronised, `version_mask` = in_version_mask WHERE `id` = in_id; IF ROW_COUNT() <> 1 THEN SET out_status = 'NoChange'; ELSE SET out_status = 'Updated'; END IF; CALL Image_StorePullHistory(in_id, in_pulls, out_status); SELECT * FROM ImageKey_View WHERE `ImageId` = in_id; END IF; END // CREATE OR REPLACE PROCEDURE Image_StoreCoreMetaData ( in_id INT, in_category VARCHAR(255), in_base_image VARCHAR(255), in_icon_url VARCHAR(1000), OUT out_status ENUM('Inserted', 'Updated', 'NoChange') ) BEGIN -- Only add core metadata if it has been provided with at least one value IF ( in_category IS NOT NULL OR in_base_image IS NOT NULL OR in_icon_url IS NOT NULL ) THEN IF NOT EXISTS(SELECT 1 FROM ImageMetadata WHERE `image_id` = in_id) THEN INSERT INTO ImageMetadata ( `image_id`, `category`, `base_image`, `icon_url` ) VALUES ( in_id, in_category, in_base_image, in_icon_url ); SET out_status = 'Inserted'; ELSE UPDATE ImageMetadata SET `category` = in_category, `base_image` = in_base_image, `icon_url` = in_icon_url WHERE `image_id` = in_id; IF ROW_COUNT() = 0 THEN SET out_status = 'NoChange'; ELSE SET out_status = 'Updated'; END IF; END IF; END IF; END; // CREATE TABLE ExternalUrl ( `id` INT NOT NULL auto_increment PRIMARY KEY, `image_id` INT NOT NULL, `type` ENUM('Support', 'Application', 'Donation', 'Misc'), `name` VARCHAR(255) NOT NULL, `path` VARCHAR(1000), UNIQUE KEY (`image_id`, `id`), FOREIGN KEY (`image_id`) REFERENCES Image(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; // CREATE OR REPLACE PROCEDURE Image_StoreExternalUrl ( in_image_id INT, in_id INT, in_type ENUM('Support', 'Application', 'Donation', 'Misc'), in_name VARCHAR(255), in_path VARCHAR(1000) ) BEGIN IF in_id IS NULL OR in_id = -1 THEN INSERT INTO ExternalUrl (image_id, type, name, path) VALUES ( in_image_id, in_type, in_name, in_path ); ELSE UPDATE ExternalUrl SET `type` = in_type, `name` = in_name, `path` = in_path WHERE `image_id` = in_image_id AND `id` = in_id; END IF; END; // CREATE OR REPLACE PROCEDURE Image_GetExternalUrls ( in_id INT ) BEGIN SELECT `id` AS UrlId, `type` AS UrlType, `name` AS `UrlName`, `path` AS `UrlPath` FROM ExternalUrl WHERE `image_id` = in_id; END; // CREATE OR REPLACE PROCEDURE `Image_RemoveOrphanUrls` ( in_image_id VARCHAR(255), in_urlIds VARCHAR(255) ) BEGIN DELETE FROM ExternalUrl WHERE `image_id` = in_image_id AND NOT FIND_IN_SET(`id`, in_urlIds); END; // DELIMITER ; ================================================ FILE: src/main/resources/db/migration/V2.6__AddCleanSchedule.sql ================================================ INSERT INTO Schedule (`name`, `interval`, `delay`, `java_class`) VALUE ('CleanRemovedImages', '1:hours', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.sync.CleanRemovedImagesSchedule'); ================================================ FILE: src/main/resources/db/migration/V2.7__UpdateImageTemplateSprocs.sql ================================================ DELIMITER // ALTER TABLE ImageTemplateEnvironment ADD COLUMN `example` VARCHAR(255); // CREATE OR REPLACE PROCEDURE `Image_StoreTemplateEnv` ( in_image_id INT, in_env_key VARCHAR(100), in_env_desc VARCHAR(255), in_env_example VARCHAR(255) ) BEGIN IF EXISTS(SELECT 1 FROM `ImageTemplateEnvironment` WHERE `image_id` = in_image_id AND `env_key` = in_env_key) THEN UPDATE `ImageTemplateEnvironment` SET `description` = in_env_desc, `example` = in_env_example WHERE `image_id` = in_image_id AND `env_key` = in_env_key; ELSE INSERT INTO `ImageTemplateEnvironment` ( `image_id`, `env_key`, `description`, `example` ) VALUES ( in_image_id, in_env_key, in_env_desc, in_env_example ); END IF; END // CREATE OR REPLACE PROCEDURE `Image_GetTemplates` ( in_image_id INT ) BEGIN SELECT 'Port' AS `ItemType`, `port` AS `ItemName`, `description` AS `ItemDescription`, `protocol` AS `ItemSecondary` FROM `ImageTemplatePorts` WHERE `image_id` = in_image_id UNION ALL SELECT 'Volume' AS `ItemType`, `volume` AS `ItemName`, `description` AS `ItemDescription`, `read_only` AS `ItemSecondary` FROM `ImageTemplateVolumes` WHERE `image_id` = in_image_id UNION ALL SELECT 'Env' AS `ItemType`, `env_key` AS `ItemName`, `description` AS `ItemDescription`, `example` AS `ItemSecondary` FROM `ImageTemplateEnvironment` WHERE `image_id` = in_image_id UNION ALL SELECT 'Device' AS `ItemType`, `device` AS `ItemName`, `description` AS `ItemDescription`, NULL AS `ItemSecondary` FROM `ImageTemplateDevices` WHERE `image_id` = in_image_id UNION ALL SELECT 'Extra' AS `ItemType`, `extra_key` AS `ItemName`, `description` AS `ItemDescription`, NULL AS `ItemSecondary` FROM `ImageTemplateExtra` WHERE `image_id` = in_image_id; END // ================================================ FILE: src/main/resources/db/migration/V2.8__FixStoreCoreMetaOutputBug.sql ================================================ DELIMITER // CREATE OR REPLACE PROCEDURE Image_StoreCoreMetaData ( in_id INT, in_category VARCHAR(255), in_base_image VARCHAR(255), in_icon_url VARCHAR(1000), OUT out_status ENUM('Inserted', 'Updated', 'NoChange') ) BEGIN -- Only add core metadata if it has been provided with at least one value IF ( in_category IS NOT NULL OR in_base_image IS NOT NULL OR in_icon_url IS NOT NULL ) THEN IF NOT EXISTS(SELECT 1 FROM ImageMetadata WHERE `image_id` = in_id) THEN INSERT INTO ImageMetadata ( `image_id`, `category`, `base_image`, `icon_url` ) VALUES ( in_id, in_category, in_base_image, in_icon_url ); SET out_status = 'Inserted'; ELSE UPDATE ImageMetadata SET `category` = in_category, `base_image` = in_base_image, `icon_url` = in_icon_url WHERE `image_id` = in_id; IF ROW_COUNT() = 0 THEN SET out_status = 'NoChange'; ELSE SET out_status = 'Updated'; END IF; END IF; ELSE SET out_status = 'NoChange'; END IF; END; // ================================================ FILE: src/main/resources/static/assets/css/app.css ================================================ @media screen and (max-width: 768px) { button.is-wide-mobile { width: 100%; } } .title { font-family: 'Nunito', sans-serif !important; font-weight: 600; letter-spacing: .01rem; } .has-text-small { font-size: 0.85rem; } .card { box-shadow: 0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.05); } .has-text-weight-500 { font-weight: 500 !important; } .has-text-weight-600 { font-weight: 600 !important; } span.icon i.fas, span.icon i.far, span.icon i.fab { margin: 0; } i.fas, i.far, i.fab { margin-right: .5rem; } .navbar-title { font-size: 1.8rem; } .has-icon-large i.fas, .has-icon-large i.far, .has-icon-large i.fab { font-size: 3rem; } .media-content { overflow-x: inherit !important; } span.image-title { font-size: 1.2rem; } .has-margin-left { margin-left: 1rem; } .has-margin-right { margin-right: 1rem; } .has-margin-top { margin-top: 1rem; } .has-margin-bottom { margin-bottom: 1rem; } .is-paddingless-top { padding-top: 0 !important; } .is-paddingless-bottom { padding-bottom: 0 !important; } /* The switch - the box around the slider */ .switch { position: relative; display: inline-block; width: 30px; height: 20px; margin: 0; } .switch.switch.is-large { height: 26px; width: 40px; } /* Hide default HTML checkbox */ .switch input { opacity: 0; width: 0; height: 0; } /* The slider */ .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #e0e0e0; -webkit-transition: .4s; transition: .4s; } .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; -webkit-transition: .4s; transition: .4s; box-shadow: 1px 1px 2px #98a0a6; } .switch.is-primary input:checked + .slider { background-color: #00d1b2; } .switch.is-danger input:checked + .slider { background-color: #ff3860; } .switch.is-warning input:checked + .slider { background-color: #ffdd57; } .switch.is-info input:checked + .slider { background-color: #209cee; } .switch.is-link input:checked + .slider { background-color: #3273dc; } .switch.is-success input:checked + .slider { background-color: #23d160 } .switch.is-large .slider:before { height: 22px; width: 22px; left: 2px; bottom: 2px; } input:disabled + .slider { background-color: #f7f7f7; } input:disabled + .slider:before { box-shadow: none; } input:checked + .slider:before { -webkit-transform: translateX(10px); -ms-transform: translateX(10px); transform: translateX(10px); } .switch.is-large input:checked + .slider:before { -webkit-transform: translateX(14px); -ms-transform: translateX(14px); transform: translateX(14px); } .slider.round { border-radius: 26px; } .slider.round:before { border-radius: 50%; } table tbody .is-half { width: 50%; } table tbody tr td.is-vcentered { vertical-align: middle; } #ImageTable tr td { white-space: nowrap; } table tr th.tablesorter-header { outline: none; cursor: pointer; } .has-switchable .switchable.plaintext { border-bottom: dotted 2px; } .has-switchable .switchable.field { display: none; } .has-switchable.is-active .switchable.plaintext { display: none; } .has-switchable.is-active .switchable.field { display: flex; } .is-pointer { cursor: pointer; } .is-borderless { border: 0 !important; } #NotificationWrapper { position: relative; margin: 0 10px 0 10px; z-index: 1000; } #Notifications { position: absolute; right: 0; max-width: 100%; } .columns.has-tabs-content > .column { display: none; } .columns.has-tabs-content > .column.is-active { display: block; } #ImageTemplatePorts .template-port { width: 200px !important; } #ImageTemplatePorts .template-port-protocol { width: 150px !important; } #ImageTemplatePorts .template-port-description { min-width: 300px !important; } #ImageTemplatePorts .template-port-delete { width: 50px !important; } ================================================ FILE: src/main/resources/static/assets/css/bootstrap.css ================================================ /*! * Bootstrap v4.1.3 (https://getbootstrap.com/) * Copyright 2011-2018 The Bootstrap Authors * Copyright 2011-2018 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) */ :root { --blue: #007bff; --indigo: #6610f2; --purple: #6f42c1; --pink: #e83e8c; --red: #dc3545; --orange: #fd7e14; --yellow: #ffc107; --green: #28a745; --teal: #20c997; --cyan: #17a2b8; --white: #fff; --gray: #6c757d; --gray-dark: #343a40; --primary: #007bff; --secondary: #6c757d; --success: #28a745; --info: #17a2b8; --warning: #ffc107; --danger: #dc3545; --light: #f8f9fa; --dark: #343a40; --breakpoint-xs: 0; --breakpoint-sm: 576px; --breakpoint-md: 768px; --breakpoint-lg: 992px; --breakpoint-xl: 1200px; --font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } *, *::before, *::after { box-sizing: border-box; } html { font-family: sans-serif; line-height: 1.15; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; -ms-overflow-style: scrollbar; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } @-ms-viewport { width: device-width; } article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { display: block; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #212529; text-align: left; background-color: #fff; } [tabindex="-1"]:focus { outline: 0 !important; } hr { box-sizing: content-box; height: 0; overflow: visible; } h1, h2, h3, h4, h5, h6 { margin-top: 0; margin-bottom: 0.5rem; } p { margin-top: 0; margin-bottom: 1rem; } abbr[title], abbr[data-original-title] { text-decoration: underline; -webkit-text-decoration: underline dotted; text-decoration: underline dotted; cursor: help; border-bottom: 0; } address { margin-bottom: 1rem; font-style: normal; line-height: inherit; } ol, ul, dl { margin-top: 0; margin-bottom: 1rem; } ol ol, ul ul, ol ul, ul ol { margin-bottom: 0; } dt { font-weight: 700; } dd { margin-bottom: .5rem; margin-left: 0; } blockquote { margin: 0 0 1rem; } dfn { font-style: italic; } b, strong { font-weight: bolder; } small { font-size: 80%; } sub, sup { position: relative; font-size: 75%; line-height: 0; vertical-align: baseline; } sub { bottom: -.25em; } sup { top: -.5em; } a { color: #007bff; text-decoration: none; background-color: transparent; -webkit-text-decoration-skip: objects; } a:hover { color: #0056b3; text-decoration: underline; } a:not([href]):not([tabindex]) { color: inherit; text-decoration: none; } a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { color: inherit; text-decoration: none; } a:not([href]):not([tabindex]):focus { outline: 0; } pre, code, kbd, samp { font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 1em; } pre { margin-top: 0; margin-bottom: 1rem; overflow: auto; -ms-overflow-style: scrollbar; } figure { margin: 0 0 1rem; } img { vertical-align: middle; border-style: none; } svg { overflow: hidden; vertical-align: middle; } table { border-collapse: collapse; } caption { padding-top: 0.75rem; padding-bottom: 0.75rem; color: #6c757d; text-align: left; caption-side: bottom; } th { text-align: inherit; } label { display: inline-block; margin-bottom: 0.5rem; } button { border-radius: 0; } button:focus { outline: 1px dotted; outline: 5px auto -webkit-focus-ring-color; } input, button, select, optgroup, textarea { margin: 0; font-family: inherit; font-size: inherit; line-height: inherit; } button, input { overflow: visible; } button, select { text-transform: none; } button, html [type="button"], [type="reset"], [type="submit"] { -webkit-appearance: button; } button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { padding: 0; border-style: none; } input[type="radio"], input[type="checkbox"] { box-sizing: border-box; padding: 0; } input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"] { -webkit-appearance: listbox; } textarea { overflow: auto; resize: vertical; } fieldset { min-width: 0; padding: 0; margin: 0; border: 0; } legend { display: block; width: 100%; max-width: 100%; padding: 0; margin-bottom: .5rem; font-size: 1.5rem; line-height: inherit; color: inherit; white-space: normal; } progress { vertical-align: baseline; } [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } [type="search"] { outline-offset: -2px; -webkit-appearance: none; } [type="search"]::-webkit-search-cancel-button, [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } ::-webkit-file-upload-button { font: inherit; -webkit-appearance: button; } output { display: inline-block; } summary { display: list-item; cursor: pointer; } template { display: none; } [hidden] { display: none !important; } h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { margin-bottom: 0.5rem; font-family: inherit; font-weight: 500; line-height: 1.2; color: inherit; } h1, .h1 { font-size: 2.5rem; } h2, .h2 { font-size: 2rem; } h3, .h3 { font-size: 1.75rem; } h4, .h4 { font-size: 1.5rem; } h5, .h5 { font-size: 1.25rem; } h6, .h6 { font-size: 1rem; } .lead { font-size: 1.25rem; font-weight: 300; } .display-1 { font-size: 6rem; font-weight: 300; line-height: 1.2; } .display-2 { font-size: 5.5rem; font-weight: 300; line-height: 1.2; } .display-3 { font-size: 4.5rem; font-weight: 300; line-height: 1.2; } .display-4 { font-size: 3.5rem; font-weight: 300; line-height: 1.2; } hr { margin-top: 1rem; margin-bottom: 1rem; border: 0; border-top: 1px solid rgba(0, 0, 0, 0.1); } small, .small { font-size: 80%; font-weight: 400; } mark, .mark { padding: 0.2em; background-color: #fcf8e3; } .list-unstyled { padding-left: 0; list-style: none; } .list-inline { padding-left: 0; list-style: none; } .list-inline-item { display: inline-block; } .list-inline-item:not(:last-child) { margin-right: 0.5rem; } .initialism { font-size: 90%; text-transform: uppercase; } .blockquote { margin-bottom: 1rem; font-size: 1.25rem; } .blockquote-footer { display: block; font-size: 80%; color: #6c757d; } .blockquote-footer::before { content: "\2014 \00A0"; } .img-fluid { max-width: 100%; height: auto; } .img-thumbnail { padding: 0.25rem; background-color: #fff; border: 1px solid #dee2e6; border-radius: 0.25rem; max-width: 100%; height: auto; } .figure { display: inline-block; } .figure-img { margin-bottom: 0.5rem; line-height: 1; } .figure-caption { font-size: 90%; color: #6c757d; } code { font-size: 87.5%; color: #e83e8c; word-break: break-word; } a > code { color: inherit; } kbd { padding: 0.2rem 0.4rem; font-size: 87.5%; color: #fff; background-color: #212529; border-radius: 0.2rem; } kbd kbd { padding: 0; font-size: 100%; font-weight: 700; } pre { display: block; font-size: 87.5%; color: #212529; } pre code { font-size: inherit; color: inherit; word-break: normal; } .pre-scrollable { max-height: 340px; overflow-y: scroll; } .container { width: 100%; padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } @media (min-width: 576px) { .container { max-width: 540px; } } @media (min-width: 768px) { .container { max-width: 720px; } } @media (min-width: 992px) { .container { max-width: 960px; } } @media (min-width: 1200px) { .container { max-width: 1140px; } } .container-fluid { width: 100%; padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } .row { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; margin-right: -15px; margin-left: -15px; } .no-gutters { margin-right: 0; margin-left: 0; } .no-gutters > .col, .no-gutters > [class*="col-"] { padding-right: 0; padding-left: 0; } .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col, .col-auto, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm, .col-sm-auto, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12, .col-md, .col-md-auto, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg, .col-lg-auto, .col-xl-1, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl, .col-xl-auto { position: relative; width: 100%; min-height: 1px; padding-right: 15px; padding-left: 15px; } .col { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; max-width: 100%; } .col-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: none; } .col-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-first { -ms-flex-order: -1; order: -1; } .order-last { -ms-flex-order: 13; order: 13; } .order-0 { -ms-flex-order: 0; order: 0; } .order-1 { -ms-flex-order: 1; order: 1; } .order-2 { -ms-flex-order: 2; order: 2; } .order-3 { -ms-flex-order: 3; order: 3; } .order-4 { -ms-flex-order: 4; order: 4; } .order-5 { -ms-flex-order: 5; order: 5; } .order-6 { -ms-flex-order: 6; order: 6; } .order-7 { -ms-flex-order: 7; order: 7; } .order-8 { -ms-flex-order: 8; order: 8; } .order-9 { -ms-flex-order: 9; order: 9; } .order-10 { -ms-flex-order: 10; order: 10; } .order-11 { -ms-flex-order: 11; order: 11; } .order-12 { -ms-flex-order: 12; order: 12; } .offset-1 { margin-left: 8.333333%; } .offset-2 { margin-left: 16.666667%; } .offset-3 { margin-left: 25%; } .offset-4 { margin-left: 33.333333%; } .offset-5 { margin-left: 41.666667%; } .offset-6 { margin-left: 50%; } .offset-7 { margin-left: 58.333333%; } .offset-8 { margin-left: 66.666667%; } .offset-9 { margin-left: 75%; } .offset-10 { margin-left: 83.333333%; } .offset-11 { margin-left: 91.666667%; } @media (min-width: 576px) { .col-sm { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; max-width: 100%; } .col-sm-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: none; } .col-sm-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-sm-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-sm-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-sm-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-sm-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-sm-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-sm-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-sm-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-sm-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-sm-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-sm-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-sm-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-sm-first { -ms-flex-order: -1; order: -1; } .order-sm-last { -ms-flex-order: 13; order: 13; } .order-sm-0 { -ms-flex-order: 0; order: 0; } .order-sm-1 { -ms-flex-order: 1; order: 1; } .order-sm-2 { -ms-flex-order: 2; order: 2; } .order-sm-3 { -ms-flex-order: 3; order: 3; } .order-sm-4 { -ms-flex-order: 4; order: 4; } .order-sm-5 { -ms-flex-order: 5; order: 5; } .order-sm-6 { -ms-flex-order: 6; order: 6; } .order-sm-7 { -ms-flex-order: 7; order: 7; } .order-sm-8 { -ms-flex-order: 8; order: 8; } .order-sm-9 { -ms-flex-order: 9; order: 9; } .order-sm-10 { -ms-flex-order: 10; order: 10; } .order-sm-11 { -ms-flex-order: 11; order: 11; } .order-sm-12 { -ms-flex-order: 12; order: 12; } .offset-sm-0 { margin-left: 0; } .offset-sm-1 { margin-left: 8.333333%; } .offset-sm-2 { margin-left: 16.666667%; } .offset-sm-3 { margin-left: 25%; } .offset-sm-4 { margin-left: 33.333333%; } .offset-sm-5 { margin-left: 41.666667%; } .offset-sm-6 { margin-left: 50%; } .offset-sm-7 { margin-left: 58.333333%; } .offset-sm-8 { margin-left: 66.666667%; } .offset-sm-9 { margin-left: 75%; } .offset-sm-10 { margin-left: 83.333333%; } .offset-sm-11 { margin-left: 91.666667%; } } @media (min-width: 768px) { .col-md { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; max-width: 100%; } .col-md-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: none; } .col-md-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-md-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-md-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-md-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-md-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-md-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-md-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-md-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-md-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-md-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-md-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-md-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-md-first { -ms-flex-order: -1; order: -1; } .order-md-last { -ms-flex-order: 13; order: 13; } .order-md-0 { -ms-flex-order: 0; order: 0; } .order-md-1 { -ms-flex-order: 1; order: 1; } .order-md-2 { -ms-flex-order: 2; order: 2; } .order-md-3 { -ms-flex-order: 3; order: 3; } .order-md-4 { -ms-flex-order: 4; order: 4; } .order-md-5 { -ms-flex-order: 5; order: 5; } .order-md-6 { -ms-flex-order: 6; order: 6; } .order-md-7 { -ms-flex-order: 7; order: 7; } .order-md-8 { -ms-flex-order: 8; order: 8; } .order-md-9 { -ms-flex-order: 9; order: 9; } .order-md-10 { -ms-flex-order: 10; order: 10; } .order-md-11 { -ms-flex-order: 11; order: 11; } .order-md-12 { -ms-flex-order: 12; order: 12; } .offset-md-0 { margin-left: 0; } .offset-md-1 { margin-left: 8.333333%; } .offset-md-2 { margin-left: 16.666667%; } .offset-md-3 { margin-left: 25%; } .offset-md-4 { margin-left: 33.333333%; } .offset-md-5 { margin-left: 41.666667%; } .offset-md-6 { margin-left: 50%; } .offset-md-7 { margin-left: 58.333333%; } .offset-md-8 { margin-left: 66.666667%; } .offset-md-9 { margin-left: 75%; } .offset-md-10 { margin-left: 83.333333%; } .offset-md-11 { margin-left: 91.666667%; } } @media (min-width: 992px) { .col-lg { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; max-width: 100%; } .col-lg-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: none; } .col-lg-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-lg-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-lg-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-lg-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-lg-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-lg-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-lg-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-lg-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-lg-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-lg-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-lg-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-lg-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-lg-first { -ms-flex-order: -1; order: -1; } .order-lg-last { -ms-flex-order: 13; order: 13; } .order-lg-0 { -ms-flex-order: 0; order: 0; } .order-lg-1 { -ms-flex-order: 1; order: 1; } .order-lg-2 { -ms-flex-order: 2; order: 2; } .order-lg-3 { -ms-flex-order: 3; order: 3; } .order-lg-4 { -ms-flex-order: 4; order: 4; } .order-lg-5 { -ms-flex-order: 5; order: 5; } .order-lg-6 { -ms-flex-order: 6; order: 6; } .order-lg-7 { -ms-flex-order: 7; order: 7; } .order-lg-8 { -ms-flex-order: 8; order: 8; } .order-lg-9 { -ms-flex-order: 9; order: 9; } .order-lg-10 { -ms-flex-order: 10; order: 10; } .order-lg-11 { -ms-flex-order: 11; order: 11; } .order-lg-12 { -ms-flex-order: 12; order: 12; } .offset-lg-0 { margin-left: 0; } .offset-lg-1 { margin-left: 8.333333%; } .offset-lg-2 { margin-left: 16.666667%; } .offset-lg-3 { margin-left: 25%; } .offset-lg-4 { margin-left: 33.333333%; } .offset-lg-5 { margin-left: 41.666667%; } .offset-lg-6 { margin-left: 50%; } .offset-lg-7 { margin-left: 58.333333%; } .offset-lg-8 { margin-left: 66.666667%; } .offset-lg-9 { margin-left: 75%; } .offset-lg-10 { margin-left: 83.333333%; } .offset-lg-11 { margin-left: 91.666667%; } } @media (min-width: 1200px) { .col-xl { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; max-width: 100%; } .col-xl-auto { -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: none; } .col-xl-1 { -ms-flex: 0 0 8.333333%; flex: 0 0 8.333333%; max-width: 8.333333%; } .col-xl-2 { -ms-flex: 0 0 16.666667%; flex: 0 0 16.666667%; max-width: 16.666667%; } .col-xl-3 { -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; } .col-xl-4 { -ms-flex: 0 0 33.333333%; flex: 0 0 33.333333%; max-width: 33.333333%; } .col-xl-5 { -ms-flex: 0 0 41.666667%; flex: 0 0 41.666667%; max-width: 41.666667%; } .col-xl-6 { -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; } .col-xl-7 { -ms-flex: 0 0 58.333333%; flex: 0 0 58.333333%; max-width: 58.333333%; } .col-xl-8 { -ms-flex: 0 0 66.666667%; flex: 0 0 66.666667%; max-width: 66.666667%; } .col-xl-9 { -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; } .col-xl-10 { -ms-flex: 0 0 83.333333%; flex: 0 0 83.333333%; max-width: 83.333333%; } .col-xl-11 { -ms-flex: 0 0 91.666667%; flex: 0 0 91.666667%; max-width: 91.666667%; } .col-xl-12 { -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; } .order-xl-first { -ms-flex-order: -1; order: -1; } .order-xl-last { -ms-flex-order: 13; order: 13; } .order-xl-0 { -ms-flex-order: 0; order: 0; } .order-xl-1 { -ms-flex-order: 1; order: 1; } .order-xl-2 { -ms-flex-order: 2; order: 2; } .order-xl-3 { -ms-flex-order: 3; order: 3; } .order-xl-4 { -ms-flex-order: 4; order: 4; } .order-xl-5 { -ms-flex-order: 5; order: 5; } .order-xl-6 { -ms-flex-order: 6; order: 6; } .order-xl-7 { -ms-flex-order: 7; order: 7; } .order-xl-8 { -ms-flex-order: 8; order: 8; } .order-xl-9 { -ms-flex-order: 9; order: 9; } .order-xl-10 { -ms-flex-order: 10; order: 10; } .order-xl-11 { -ms-flex-order: 11; order: 11; } .order-xl-12 { -ms-flex-order: 12; order: 12; } .offset-xl-0 { margin-left: 0; } .offset-xl-1 { margin-left: 8.333333%; } .offset-xl-2 { margin-left: 16.666667%; } .offset-xl-3 { margin-left: 25%; } .offset-xl-4 { margin-left: 33.333333%; } .offset-xl-5 { margin-left: 41.666667%; } .offset-xl-6 { margin-left: 50%; } .offset-xl-7 { margin-left: 58.333333%; } .offset-xl-8 { margin-left: 66.666667%; } .offset-xl-9 { margin-left: 75%; } .offset-xl-10 { margin-left: 83.333333%; } .offset-xl-11 { margin-left: 91.666667%; } } .table { width: 100%; margin-bottom: 1rem; background-color: transparent; } .table th, .table td { padding: 0.75rem; vertical-align: top; border-top: 1px solid #dee2e6; } .table thead th { vertical-align: bottom; border-bottom: 2px solid #dee2e6; } .table tbody + tbody { border-top: 2px solid #dee2e6; } .table .table { background-color: #fff; } .table-sm th, .table-sm td { padding: 0.3rem; } .table-bordered { border: 1px solid #dee2e6; } .table-bordered th, .table-bordered td { border: 1px solid #dee2e6; } .table-bordered thead th, .table-bordered thead td { border-bottom-width: 2px; } .table-borderless th, .table-borderless td, .table-borderless thead th, .table-borderless tbody + tbody { border: 0; } .table-striped tbody tr:nth-of-type(odd) { background-color: rgba(0, 0, 0, 0.05); } .table-hover tbody tr:hover { background-color: rgba(0, 0, 0, 0.075); } .table-primary, .table-primary > th, .table-primary > td { background-color: #b8daff; } .table-hover .table-primary:hover { background-color: #9fcdff; } .table-hover .table-primary:hover > td, .table-hover .table-primary:hover > th { background-color: #9fcdff; } .table-secondary, .table-secondary > th, .table-secondary > td { background-color: #d6d8db; } .table-hover .table-secondary:hover { background-color: #c8cbcf; } .table-hover .table-secondary:hover > td, .table-hover .table-secondary:hover > th { background-color: #c8cbcf; } .table-success, .table-success > th, .table-success > td { background-color: #c3e6cb; } .table-hover .table-success:hover { background-color: #b1dfbb; } .table-hover .table-success:hover > td, .table-hover .table-success:hover > th { background-color: #b1dfbb; } .table-info, .table-info > th, .table-info > td { background-color: #bee5eb; } .table-hover .table-info:hover { background-color: #abdde5; } .table-hover .table-info:hover > td, .table-hover .table-info:hover > th { background-color: #abdde5; } .table-warning, .table-warning > th, .table-warning > td { background-color: #ffeeba; } .table-hover .table-warning:hover { background-color: #ffe8a1; } .table-hover .table-warning:hover > td, .table-hover .table-warning:hover > th { background-color: #ffe8a1; } .table-danger, .table-danger > th, .table-danger > td { background-color: #f5c6cb; } .table-hover .table-danger:hover { background-color: #f1b0b7; } .table-hover .table-danger:hover > td, .table-hover .table-danger:hover > th { background-color: #f1b0b7; } .table-light, .table-light > th, .table-light > td { background-color: #fdfdfe; } .table-hover .table-light:hover { background-color: #ececf6; } .table-hover .table-light:hover > td, .table-hover .table-light:hover > th { background-color: #ececf6; } .table-dark, .table-dark > th, .table-dark > td { background-color: #c6c8ca; } .table-hover .table-dark:hover { background-color: #b9bbbe; } .table-hover .table-dark:hover > td, .table-hover .table-dark:hover > th { background-color: #b9bbbe; } .table-active, .table-active > th, .table-active > td { background-color: rgba(0, 0, 0, 0.075); } .table-hover .table-active:hover { background-color: rgba(0, 0, 0, 0.075); } .table-hover .table-active:hover > td, .table-hover .table-active:hover > th { background-color: rgba(0, 0, 0, 0.075); } .table .thead-dark th { color: #fff; background-color: #212529; border-color: #32383e; } .table .thead-light th { color: #495057; background-color: #e9ecef; border-color: #dee2e6; } .table-dark { color: #fff; background-color: #212529; } .table-dark th, .table-dark td, .table-dark thead th { border-color: #32383e; } .table-dark.table-bordered { border: 0; } .table-dark.table-striped tbody tr:nth-of-type(odd) { background-color: rgba(255, 255, 255, 0.05); } .table-dark.table-hover tbody tr:hover { background-color: rgba(255, 255, 255, 0.075); } @media (max-width: 575.98px) { .table-responsive-sm { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } .table-responsive-sm > .table-bordered { border: 0; } } @media (max-width: 767.98px) { .table-responsive-md { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } .table-responsive-md > .table-bordered { border: 0; } } @media (max-width: 991.98px) { .table-responsive-lg { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } .table-responsive-lg > .table-bordered { border: 0; } } @media (max-width: 1199.98px) { .table-responsive-xl { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } .table-responsive-xl > .table-bordered { border: 0; } } .table-responsive { display: block; width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } .table-responsive > .table-bordered { border: 0; } .form-control { display: block; width: 100%; height: calc(2.25rem + 2px); padding: 0.375rem 0.75rem; font-size: 1rem; line-height: 1.5; color: #495057; background-color: #fff; background-clip: padding-box; border: 1px solid #ced4da; border-radius: 0.25rem; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media screen and (prefers-reduced-motion: reduce) { .form-control { transition: none; } } .form-control::-ms-expand { background-color: transparent; border: 0; } .form-control:focus { color: #495057; background-color: #fff; border-color: #80bdff; outline: 0; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .form-control::-webkit-input-placeholder { color: #6c757d; opacity: 1; } .form-control::-moz-placeholder { color: #6c757d; opacity: 1; } .form-control:-ms-input-placeholder { color: #6c757d; opacity: 1; } .form-control::-ms-input-placeholder { color: #6c757d; opacity: 1; } .form-control::placeholder { color: #6c757d; opacity: 1; } .form-control:disabled, .form-control[readonly] { background-color: #e9ecef; opacity: 1; } select.form-control:focus::-ms-value { color: #495057; background-color: #fff; } .form-control-file, .form-control-range { display: block; width: 100%; } .col-form-label { padding-top: calc(0.375rem + 1px); padding-bottom: calc(0.375rem + 1px); margin-bottom: 0; font-size: inherit; line-height: 1.5; } .col-form-label-lg { padding-top: calc(0.5rem + 1px); padding-bottom: calc(0.5rem + 1px); font-size: 1.25rem; line-height: 1.5; } .col-form-label-sm { padding-top: calc(0.25rem + 1px); padding-bottom: calc(0.25rem + 1px); font-size: 0.875rem; line-height: 1.5; } .form-control-plaintext { display: block; width: 100%; padding-top: 0.375rem; padding-bottom: 0.375rem; margin-bottom: 0; line-height: 1.5; color: #212529; background-color: transparent; border: solid transparent; border-width: 1px 0; } .form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { padding-right: 0; padding-left: 0; } .form-control-sm { height: calc(1.8125rem + 2px); padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.5; border-radius: 0.2rem; } .form-control-lg { height: calc(2.875rem + 2px); padding: 0.5rem 1rem; font-size: 1.25rem; line-height: 1.5; border-radius: 0.3rem; } select.form-control[size], select.form-control[multiple] { height: auto; } textarea.form-control { height: auto; } .form-group { margin-bottom: 1rem; } .form-text { display: block; margin-top: 0.25rem; } .form-row { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; margin-right: -5px; margin-left: -5px; } .form-row > .col, .form-row > [class*="col-"] { padding-right: 5px; padding-left: 5px; } .form-check { position: relative; display: block; padding-left: 1.25rem; } .form-check-input { position: absolute; margin-top: 0.3rem; margin-left: -1.25rem; } .form-check-input:disabled ~ .form-check-label { color: #6c757d; } .form-check-label { margin-bottom: 0; } .form-check-inline { display: -ms-inline-flexbox; display: inline-flex; -ms-flex-align: center; align-items: center; padding-left: 0; margin-right: 0.75rem; } .form-check-inline .form-check-input { position: static; margin-top: 0; margin-right: 0.3125rem; margin-left: 0; } .valid-feedback { display: none; width: 100%; margin-top: 0.25rem; font-size: 80%; color: #28a745; } .valid-tooltip { position: absolute; top: 100%; z-index: 5; display: none; max-width: 100%; padding: 0.25rem 0.5rem; margin-top: .1rem; font-size: 0.875rem; line-height: 1.5; color: #fff; background-color: rgba(40, 167, 69, 0.9); border-radius: 0.25rem; } .was-validated .form-control:valid, .form-control.is-valid, .was-validated .custom-select:valid, .custom-select.is-valid { border-color: #28a745; } .was-validated .form-control:valid:focus, .form-control.is-valid:focus, .was-validated .custom-select:valid:focus, .custom-select.is-valid:focus { border-color: #28a745; box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .was-validated .form-control:valid ~ .valid-feedback, .was-validated .form-control:valid ~ .valid-tooltip, .form-control.is-valid ~ .valid-feedback, .form-control.is-valid ~ .valid-tooltip, .was-validated .custom-select:valid ~ .valid-feedback, .was-validated .custom-select:valid ~ .valid-tooltip, .custom-select.is-valid ~ .valid-feedback, .custom-select.is-valid ~ .valid-tooltip { display: block; } .was-validated .form-control-file:valid ~ .valid-feedback, .was-validated .form-control-file:valid ~ .valid-tooltip, .form-control-file.is-valid ~ .valid-feedback, .form-control-file.is-valid ~ .valid-tooltip { display: block; } .was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { color: #28a745; } .was-validated .form-check-input:valid ~ .valid-feedback, .was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback, .form-check-input.is-valid ~ .valid-tooltip { display: block; } .was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label { color: #28a745; } .was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before { background-color: #71dd8a; } .was-validated .custom-control-input:valid ~ .valid-feedback, .was-validated .custom-control-input:valid ~ .valid-tooltip, .custom-control-input.is-valid ~ .valid-feedback, .custom-control-input.is-valid ~ .valid-tooltip { display: block; } .was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before { background-color: #34ce57; } .was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label { border-color: #28a745; } .was-validated .custom-file-input:valid ~ .custom-file-label::after, .custom-file-input.is-valid ~ .custom-file-label::after { border-color: inherit; } .was-validated .custom-file-input:valid ~ .valid-feedback, .was-validated .custom-file-input:valid ~ .valid-tooltip, .custom-file-input.is-valid ~ .valid-feedback, .custom-file-input.is-valid ~ .valid-tooltip { display: block; } .was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label { box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25); } .invalid-feedback { display: none; width: 100%; margin-top: 0.25rem; font-size: 80%; color: #dc3545; } .invalid-tooltip { position: absolute; top: 100%; z-index: 5; display: none; max-width: 100%; padding: 0.25rem 0.5rem; margin-top: .1rem; font-size: 0.875rem; line-height: 1.5; color: #fff; background-color: rgba(220, 53, 69, 0.9); border-radius: 0.25rem; } .was-validated .form-control:invalid, .form-control.is-invalid, .was-validated .custom-select:invalid, .custom-select.is-invalid { border-color: #dc3545; } .was-validated .form-control:invalid:focus, .form-control.is-invalid:focus, .was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus { border-color: #dc3545; box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } .was-validated .form-control:invalid ~ .invalid-feedback, .was-validated .form-control:invalid ~ .invalid-tooltip, .form-control.is-invalid ~ .invalid-feedback, .form-control.is-invalid ~ .invalid-tooltip, .was-validated .custom-select:invalid ~ .invalid-feedback, .was-validated .custom-select:invalid ~ .invalid-tooltip, .custom-select.is-invalid ~ .invalid-feedback, .custom-select.is-invalid ~ .invalid-tooltip { display: block; } .was-validated .form-control-file:invalid ~ .invalid-feedback, .was-validated .form-control-file:invalid ~ .invalid-tooltip, .form-control-file.is-invalid ~ .invalid-feedback, .form-control-file.is-invalid ~ .invalid-tooltip { display: block; } .was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { color: #dc3545; } .was-validated .form-check-input:invalid ~ .invalid-feedback, .was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback, .form-check-input.is-invalid ~ .invalid-tooltip { display: block; } .was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label { color: #dc3545; } .was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before { background-color: #efa2a9; } .was-validated .custom-control-input:invalid ~ .invalid-feedback, .was-validated .custom-control-input:invalid ~ .invalid-tooltip, .custom-control-input.is-invalid ~ .invalid-feedback, .custom-control-input.is-invalid ~ .invalid-tooltip { display: block; } .was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before { background-color: #e4606d; } .was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } .was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label { border-color: #dc3545; } .was-validated .custom-file-input:invalid ~ .custom-file-label::after, .custom-file-input.is-invalid ~ .custom-file-label::after { border-color: inherit; } .was-validated .custom-file-input:invalid ~ .invalid-feedback, .was-validated .custom-file-input:invalid ~ .invalid-tooltip, .custom-file-input.is-invalid ~ .invalid-feedback, .custom-file-input.is-invalid ~ .invalid-tooltip { display: block; } .was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label { box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); } .form-inline { display: -ms-flexbox; display: flex; -ms-flex-flow: row wrap; flex-flow: row wrap; -ms-flex-align: center; align-items: center; } .form-inline .form-check { width: 100%; } @media (min-width: 576px) { .form-inline label { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; -ms-flex-pack: center; justify-content: center; margin-bottom: 0; } .form-inline .form-group { display: -ms-flexbox; display: flex; -ms-flex: 0 0 auto; flex: 0 0 auto; -ms-flex-flow: row wrap; flex-flow: row wrap; -ms-flex-align: center; align-items: center; margin-bottom: 0; } .form-inline .form-control { display: inline-block; width: auto; vertical-align: middle; } .form-inline .form-control-plaintext { display: inline-block; } .form-inline .input-group, .form-inline .custom-select { width: auto; } .form-inline .form-check { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; -ms-flex-pack: center; justify-content: center; width: auto; padding-left: 0; } .form-inline .form-check-input { position: relative; margin-top: 0; margin-right: 0.25rem; margin-left: 0; } .form-inline .custom-control { -ms-flex-align: center; align-items: center; -ms-flex-pack: center; justify-content: center; } .form-inline .custom-control-label { margin-bottom: 0; } } .btn { display: inline-block; font-weight: 400; text-align: center; white-space: nowrap; vertical-align: middle; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; border: 1px solid transparent; padding: 0.375rem 0.75rem; font-size: 1rem; line-height: 1.5; border-radius: 0.25rem; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media screen and (prefers-reduced-motion: reduce) { .btn { transition: none; } } .btn:hover, .btn:focus { text-decoration: none; } .btn:focus, .btn.focus { outline: 0; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .btn.disabled, .btn:disabled { opacity: 0.65; } .btn:not(:disabled):not(.disabled) { cursor: pointer; } a.btn.disabled, fieldset:disabled a.btn { pointer-events: none; } .btn-primary { color: #fff; background-color: #007bff; border-color: #007bff; } .btn-primary:hover { color: #fff; background-color: #0069d9; border-color: #0062cc; } .btn-primary:focus, .btn-primary.focus { box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } .btn-primary.disabled, .btn-primary:disabled { color: #fff; background-color: #007bff; border-color: #007bff; } .btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active, .show > .btn-primary.dropdown-toggle { color: #fff; background-color: #0062cc; border-color: #005cbf; } .btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus, .show > .btn-primary.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } .btn-secondary { color: #fff; background-color: #6c757d; border-color: #6c757d; } .btn-secondary:hover { color: #fff; background-color: #5a6268; border-color: #545b62; } .btn-secondary:focus, .btn-secondary.focus { box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } .btn-secondary.disabled, .btn-secondary:disabled { color: #fff; background-color: #6c757d; border-color: #6c757d; } .btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active, .show > .btn-secondary.dropdown-toggle { color: #fff; background-color: #545b62; border-color: #4e555b; } .btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-secondary.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } .btn-success { color: #fff; background-color: #28a745; border-color: #28a745; } .btn-success:hover { color: #fff; background-color: #218838; border-color: #1e7e34; } .btn-success:focus, .btn-success.focus { box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } .btn-success.disabled, .btn-success:disabled { color: #fff; background-color: #28a745; border-color: #28a745; } .btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, .show > .btn-success.dropdown-toggle { color: #fff; background-color: #1e7e34; border-color: #1c7430; } .btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus, .show > .btn-success.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } .btn-info { color: #fff; background-color: #17a2b8; border-color: #17a2b8; } .btn-info:hover { color: #fff; background-color: #138496; border-color: #117a8b; } .btn-info:focus, .btn-info.focus { box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } .btn-info.disabled, .btn-info:disabled { color: #fff; background-color: #17a2b8; border-color: #17a2b8; } .btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active, .show > .btn-info.dropdown-toggle { color: #fff; background-color: #117a8b; border-color: #10707f; } .btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus, .show > .btn-info.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } .btn-warning { color: #212529; background-color: #ffc107; border-color: #ffc107; } .btn-warning:hover { color: #212529; background-color: #e0a800; border-color: #d39e00; } .btn-warning:focus, .btn-warning.focus { box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } .btn-warning.disabled, .btn-warning:disabled { color: #212529; background-color: #ffc107; border-color: #ffc107; } .btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active, .show > .btn-warning.dropdown-toggle { color: #212529; background-color: #d39e00; border-color: #c69500; } .btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus, .show > .btn-warning.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } .btn-danger { color: #fff; background-color: #dc3545; border-color: #dc3545; } .btn-danger:hover { color: #fff; background-color: #c82333; border-color: #bd2130; } .btn-danger:focus, .btn-danger.focus { box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } .btn-danger.disabled, .btn-danger:disabled { color: #fff; background-color: #dc3545; border-color: #dc3545; } .btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active, .show > .btn-danger.dropdown-toggle { color: #fff; background-color: #bd2130; border-color: #b21f2d; } .btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus, .show > .btn-danger.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } .btn-light { color: #212529; background-color: #f8f9fa; border-color: #f8f9fa; } .btn-light:hover { color: #212529; background-color: #e2e6ea; border-color: #dae0e5; } .btn-light:focus, .btn-light.focus { box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } .btn-light.disabled, .btn-light:disabled { color: #212529; background-color: #f8f9fa; border-color: #f8f9fa; } .btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active, .show > .btn-light.dropdown-toggle { color: #212529; background-color: #dae0e5; border-color: #d3d9df; } .btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus, .show > .btn-light.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } .btn-dark { color: #fff; background-color: #343a40; border-color: #343a40; } .btn-dark:hover { color: #fff; background-color: #23272b; border-color: #1d2124; } .btn-dark:focus, .btn-dark.focus { box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } .btn-dark.disabled, .btn-dark:disabled { color: #fff; background-color: #343a40; border-color: #343a40; } .btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active, .show > .btn-dark.dropdown-toggle { color: #fff; background-color: #1d2124; border-color: #171a1d; } .btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus, .show > .btn-dark.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } .btn-outline-primary { color: #007bff; background-color: transparent; background-image: none; border-color: #007bff; } .btn-outline-primary:hover { color: #fff; background-color: #007bff; border-color: #007bff; } .btn-outline-primary:focus, .btn-outline-primary.focus { box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } .btn-outline-primary.disabled, .btn-outline-primary:disabled { color: #007bff; background-color: transparent; } .btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, .show > .btn-outline-primary.dropdown-toggle { color: #fff; background-color: #007bff; border-color: #007bff; } .btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-primary.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5); } .btn-outline-secondary { color: #6c757d; background-color: transparent; background-image: none; border-color: #6c757d; } .btn-outline-secondary:hover { color: #fff; background-color: #6c757d; border-color: #6c757d; } .btn-outline-secondary:focus, .btn-outline-secondary.focus { box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } .btn-outline-secondary.disabled, .btn-outline-secondary:disabled { color: #6c757d; background-color: transparent; } .btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active, .show > .btn-outline-secondary.dropdown-toggle { color: #fff; background-color: #6c757d; border-color: #6c757d; } .btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-secondary.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); } .btn-outline-success { color: #28a745; background-color: transparent; background-image: none; border-color: #28a745; } .btn-outline-success:hover { color: #fff; background-color: #28a745; border-color: #28a745; } .btn-outline-success:focus, .btn-outline-success.focus { box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } .btn-outline-success.disabled, .btn-outline-success:disabled { color: #28a745; background-color: transparent; } .btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active, .show > .btn-outline-success.dropdown-toggle { color: #fff; background-color: #28a745; border-color: #28a745; } .btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-success.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5); } .btn-outline-info { color: #17a2b8; background-color: transparent; background-image: none; border-color: #17a2b8; } .btn-outline-info:hover { color: #fff; background-color: #17a2b8; border-color: #17a2b8; } .btn-outline-info:focus, .btn-outline-info.focus { box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } .btn-outline-info.disabled, .btn-outline-info:disabled { color: #17a2b8; background-color: transparent; } .btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active, .show > .btn-outline-info.dropdown-toggle { color: #fff; background-color: #17a2b8; border-color: #17a2b8; } .btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-info.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); } .btn-outline-warning { color: #ffc107; background-color: transparent; background-image: none; border-color: #ffc107; } .btn-outline-warning:hover { color: #212529; background-color: #ffc107; border-color: #ffc107; } .btn-outline-warning:focus, .btn-outline-warning.focus { box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } .btn-outline-warning.disabled, .btn-outline-warning:disabled { color: #ffc107; background-color: transparent; } .btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active, .show > .btn-outline-warning.dropdown-toggle { color: #212529; background-color: #ffc107; border-color: #ffc107; } .btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-warning.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5); } .btn-outline-danger { color: #dc3545; background-color: transparent; background-image: none; border-color: #dc3545; } .btn-outline-danger:hover { color: #fff; background-color: #dc3545; border-color: #dc3545; } .btn-outline-danger:focus, .btn-outline-danger.focus { box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } .btn-outline-danger.disabled, .btn-outline-danger:disabled { color: #dc3545; background-color: transparent; } .btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active, .show > .btn-outline-danger.dropdown-toggle { color: #fff; background-color: #dc3545; border-color: #dc3545; } .btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-danger.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5); } .btn-outline-light { color: #f8f9fa; background-color: transparent; background-image: none; border-color: #f8f9fa; } .btn-outline-light:hover { color: #212529; background-color: #f8f9fa; border-color: #f8f9fa; } .btn-outline-light:focus, .btn-outline-light.focus { box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } .btn-outline-light.disabled, .btn-outline-light:disabled { color: #f8f9fa; background-color: transparent; } .btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active, .show > .btn-outline-light.dropdown-toggle { color: #212529; background-color: #f8f9fa; border-color: #f8f9fa; } .btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-light.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5); } .btn-outline-dark { color: #343a40; background-color: transparent; background-image: none; border-color: #343a40; } .btn-outline-dark:hover { color: #fff; background-color: #343a40; border-color: #343a40; } .btn-outline-dark:focus, .btn-outline-dark.focus { box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } .btn-outline-dark.disabled, .btn-outline-dark:disabled { color: #343a40; background-color: transparent; } .btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active, .show > .btn-outline-dark.dropdown-toggle { color: #fff; background-color: #343a40; border-color: #343a40; } .btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus, .show > .btn-outline-dark.dropdown-toggle:focus { box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5); } .btn-link { font-weight: 400; color: #007bff; background-color: transparent; } .btn-link:hover { color: #0056b3; text-decoration: underline; background-color: transparent; border-color: transparent; } .btn-link:focus, .btn-link.focus { text-decoration: underline; border-color: transparent; box-shadow: none; } .btn-link:disabled, .btn-link.disabled { color: #6c757d; pointer-events: none; } .btn-lg, .btn-group-lg > .btn { padding: 0.5rem 1rem; font-size: 1.25rem; line-height: 1.5; border-radius: 0.3rem; } .btn-sm, .btn-group-sm > .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.5; border-radius: 0.2rem; } .btn-block { display: block; width: 100%; } .btn-block + .btn-block { margin-top: 0.5rem; } input[type="submit"].btn-block, input[type="reset"].btn-block, input[type="button"].btn-block { width: 100%; } .fade { transition: opacity 0.15s linear; } @media screen and (prefers-reduced-motion: reduce) { .fade { transition: none; } } .fade:not(.show) { opacity: 0; } .collapse:not(.show) { display: none; } .collapsing { position: relative; height: 0; overflow: hidden; transition: height 0.35s ease; } @media screen and (prefers-reduced-motion: reduce) { .collapsing { transition: none; } } .dropup, .dropright, .dropdown, .dropleft { position: relative; } .dropdown-toggle::after { display: inline-block; width: 0; height: 0; margin-left: 0.255em; vertical-align: 0.255em; content: ""; border-top: 0.3em solid; border-right: 0.3em solid transparent; border-bottom: 0; border-left: 0.3em solid transparent; } .dropdown-toggle:empty::after { margin-left: 0; } .dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 1000; display: none; float: left; min-width: 10rem; padding: 0.5rem 0; margin: 0.125rem 0 0; font-size: 1rem; color: #212529; text-align: left; list-style: none; background-color: #fff; background-clip: padding-box; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 0.25rem; } .dropdown-menu-right { right: 0; left: auto; } .dropup .dropdown-menu { top: auto; bottom: 100%; margin-top: 0; margin-bottom: 0.125rem; } .dropup .dropdown-toggle::after { display: inline-block; width: 0; height: 0; margin-left: 0.255em; vertical-align: 0.255em; content: ""; border-top: 0; border-right: 0.3em solid transparent; border-bottom: 0.3em solid; border-left: 0.3em solid transparent; } .dropup .dropdown-toggle:empty::after { margin-left: 0; } .dropright .dropdown-menu { top: 0; right: auto; left: 100%; margin-top: 0; margin-left: 0.125rem; } .dropright .dropdown-toggle::after { display: inline-block; width: 0; height: 0; margin-left: 0.255em; vertical-align: 0.255em; content: ""; border-top: 0.3em solid transparent; border-right: 0; border-bottom: 0.3em solid transparent; border-left: 0.3em solid; } .dropright .dropdown-toggle:empty::after { margin-left: 0; } .dropright .dropdown-toggle::after { vertical-align: 0; } .dropleft .dropdown-menu { top: 0; right: 100%; left: auto; margin-top: 0; margin-right: 0.125rem; } .dropleft .dropdown-toggle::after { display: inline-block; width: 0; height: 0; margin-left: 0.255em; vertical-align: 0.255em; content: ""; } .dropleft .dropdown-toggle::after { display: none; } .dropleft .dropdown-toggle::before { display: inline-block; width: 0; height: 0; margin-right: 0.255em; vertical-align: 0.255em; content: ""; border-top: 0.3em solid transparent; border-right: 0.3em solid; border-bottom: 0.3em solid transparent; } .dropleft .dropdown-toggle:empty::after { margin-left: 0; } .dropleft .dropdown-toggle::before { vertical-align: 0; } .dropdown-menu[x-placement^="top"], .dropdown-menu[x-placement^="right"], .dropdown-menu[x-placement^="bottom"], .dropdown-menu[x-placement^="left"] { right: auto; bottom: auto; } .dropdown-divider { height: 0; margin: 0.5rem 0; overflow: hidden; border-top: 1px solid #e9ecef; } .dropdown-item { display: block; width: 100%; padding: 0.25rem 1.5rem; clear: both; font-weight: 400; color: #212529; text-align: inherit; white-space: nowrap; background-color: transparent; border: 0; } .dropdown-item:hover, .dropdown-item:focus { color: #16181b; text-decoration: none; background-color: #f8f9fa; } .dropdown-item.active, .dropdown-item:active { color: #fff; text-decoration: none; background-color: #007bff; } .dropdown-item.disabled, .dropdown-item:disabled { color: #6c757d; background-color: transparent; } .dropdown-menu.show { display: block; } .dropdown-header { display: block; padding: 0.5rem 1.5rem; margin-bottom: 0; font-size: 0.875rem; color: #6c757d; white-space: nowrap; } .dropdown-item-text { display: block; padding: 0.25rem 1.5rem; color: #212529; } .btn-group, .btn-group-vertical { position: relative; display: -ms-inline-flexbox; display: inline-flex; vertical-align: middle; } .btn-group > .btn, .btn-group-vertical > .btn { position: relative; -ms-flex: 0 1 auto; flex: 0 1 auto; } .btn-group > .btn:hover, .btn-group-vertical > .btn:hover { z-index: 1; } .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, .btn-group-vertical > .btn:focus, .btn-group-vertical > .btn:active, .btn-group-vertical > .btn.active { z-index: 1; } .btn-group .btn + .btn, .btn-group .btn + .btn-group, .btn-group .btn-group + .btn, .btn-group .btn-group + .btn-group, .btn-group-vertical .btn + .btn, .btn-group-vertical .btn + .btn-group, .btn-group-vertical .btn-group + .btn, .btn-group-vertical .btn-group + .btn-group { margin-left: -1px; } .btn-toolbar { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-pack: start; justify-content: flex-start; } .btn-toolbar .input-group { width: auto; } .btn-group > .btn:first-child { margin-left: 0; } .btn-group > .btn:not(:last-child):not(.dropdown-toggle), .btn-group > .btn-group:not(:last-child) > .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; } .btn-group > .btn:not(:first-child), .btn-group > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } .dropdown-toggle-split { padding-right: 0.5625rem; padding-left: 0.5625rem; } .dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropright .dropdown-toggle-split::after { margin-left: 0; } .dropleft .dropdown-toggle-split::before { margin-right: 0; } .btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { padding-right: 0.375rem; padding-left: 0.375rem; } .btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { padding-right: 0.75rem; padding-left: 0.75rem; } .btn-group-vertical { -ms-flex-direction: column; flex-direction: column; -ms-flex-align: start; align-items: flex-start; -ms-flex-pack: center; justify-content: center; } .btn-group-vertical .btn, .btn-group-vertical .btn-group { width: 100%; } .btn-group-vertical > .btn + .btn, .btn-group-vertical > .btn + .btn-group, .btn-group-vertical > .btn-group + .btn, .btn-group-vertical > .btn-group + .btn-group { margin-top: -1px; margin-left: 0; } .btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), .btn-group-vertical > .btn-group:not(:last-child) > .btn { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .btn-group-vertical > .btn:not(:first-child), .btn-group-vertical > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-top-right-radius: 0; } .btn-group-toggle > .btn, .btn-group-toggle > .btn-group > .btn { margin-bottom: 0; } .btn-group-toggle > .btn input[type="radio"], .btn-group-toggle > .btn input[type="checkbox"], .btn-group-toggle > .btn-group > .btn input[type="radio"], .btn-group-toggle > .btn-group > .btn input[type="checkbox"] { position: absolute; clip: rect(0, 0, 0, 0); pointer-events: none; } .input-group { position: relative; display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-align: stretch; align-items: stretch; width: 100%; } .input-group > .form-control, .input-group > .custom-select, .input-group > .custom-file { position: relative; -ms-flex: 1 1 auto; flex: 1 1 auto; width: 1%; margin-bottom: 0; } .input-group > .form-control + .form-control, .input-group > .form-control + .custom-select, .input-group > .form-control + .custom-file, .input-group > .custom-select + .form-control, .input-group > .custom-select + .custom-select, .input-group > .custom-select + .custom-file, .input-group > .custom-file + .form-control, .input-group > .custom-file + .custom-select, .input-group > .custom-file + .custom-file { margin-left: -1px; } .input-group > .form-control:focus, .input-group > .custom-select:focus, .input-group > .custom-file .custom-file-input:focus ~ .custom-file-label { z-index: 3; } .input-group > .custom-file .custom-file-input:focus { z-index: 4; } .input-group > .form-control:not(:last-child), .input-group > .custom-select:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group > .form-control:not(:first-child), .input-group > .custom-select:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } .input-group > .custom-file { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; } .input-group > .custom-file:not(:last-child) .custom-file-label, .input-group > .custom-file:not(:last-child) .custom-file-label::after { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group > .custom-file:not(:first-child) .custom-file-label { border-top-left-radius: 0; border-bottom-left-radius: 0; } .input-group-prepend, .input-group-append { display: -ms-flexbox; display: flex; } .input-group-prepend .btn, .input-group-append .btn { position: relative; z-index: 2; } .input-group-prepend .btn + .btn, .input-group-prepend .btn + .input-group-text, .input-group-prepend .input-group-text + .input-group-text, .input-group-prepend .input-group-text + .btn, .input-group-append .btn + .btn, .input-group-append .btn + .input-group-text, .input-group-append .input-group-text + .input-group-text, .input-group-append .input-group-text + .btn { margin-left: -1px; } .input-group-prepend { margin-right: -1px; } .input-group-append { margin-left: -1px; } .input-group-text { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; padding: 0.375rem 0.75rem; margin-bottom: 0; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #495057; text-align: center; white-space: nowrap; background-color: #e9ecef; border: 1px solid #ced4da; border-radius: 0.25rem; } .input-group-text input[type="radio"], .input-group-text input[type="checkbox"] { margin-top: 0; } .input-group-lg > .form-control, .input-group-lg > .input-group-prepend > .input-group-text, .input-group-lg > .input-group-append > .input-group-text, .input-group-lg > .input-group-prepend > .btn, .input-group-lg > .input-group-append > .btn { height: calc(2.875rem + 2px); padding: 0.5rem 1rem; font-size: 1.25rem; line-height: 1.5; border-radius: 0.3rem; } .input-group-sm > .form-control, .input-group-sm > .input-group-prepend > .input-group-text, .input-group-sm > .input-group-append > .input-group-text, .input-group-sm > .input-group-prepend > .btn, .input-group-sm > .input-group-append > .btn { height: calc(1.8125rem + 2px); padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.5; border-radius: 0.2rem; } .input-group > .input-group-prepend > .btn, .input-group > .input-group-prepend > .input-group-text, .input-group > .input-group-append:not(:last-child) > .btn, .input-group > .input-group-append:not(:last-child) > .input-group-text, .input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle), .input-group > .input-group-append:last-child > .input-group-text:not(:last-child) { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group > .input-group-append > .btn, .input-group > .input-group-append > .input-group-text, .input-group > .input-group-prepend:not(:first-child) > .btn, .input-group > .input-group-prepend:not(:first-child) > .input-group-text, .input-group > .input-group-prepend:first-child > .btn:not(:first-child), .input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } .custom-control { position: relative; display: block; min-height: 1.5rem; padding-left: 1.5rem; } .custom-control-inline { display: -ms-inline-flexbox; display: inline-flex; margin-right: 1rem; } .custom-control-input { position: absolute; z-index: -1; opacity: 0; } .custom-control-input:checked ~ .custom-control-label::before { color: #fff; background-color: #007bff; } .custom-control-input:focus ~ .custom-control-label::before { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-control-input:active ~ .custom-control-label::before { color: #fff; background-color: #b3d7ff; } .custom-control-input:disabled ~ .custom-control-label { color: #6c757d; } .custom-control-input:disabled ~ .custom-control-label::before { background-color: #e9ecef; } .custom-control-label { position: relative; margin-bottom: 0; } .custom-control-label::before { position: absolute; top: 0.25rem; left: -1.5rem; display: block; width: 1rem; height: 1rem; pointer-events: none; content: ""; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-color: #dee2e6; } .custom-control-label::after { position: absolute; top: 0.25rem; left: -1.5rem; display: block; width: 1rem; height: 1rem; content: ""; background-repeat: no-repeat; background-position: center center; background-size: 50% 50%; } .custom-checkbox .custom-control-label::before { border-radius: 0.25rem; } .custom-checkbox .custom-control-input:checked ~ .custom-control-label::before { background-color: #007bff; } .custom-checkbox .custom-control-input:checked ~ .custom-control-label::after { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"); } .custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before { background-color: #007bff; } .custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E"); } .custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before { background-color: rgba(0, 123, 255, 0.5); } .custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before { background-color: rgba(0, 123, 255, 0.5); } .custom-radio .custom-control-label::before { border-radius: 50%; } .custom-radio .custom-control-input:checked ~ .custom-control-label::before { background-color: #007bff; } .custom-radio .custom-control-input:checked ~ .custom-control-label::after { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E"); } .custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before { background-color: rgba(0, 123, 255, 0.5); } .custom-select { display: inline-block; width: 100%; height: calc(2.25rem + 2px); padding: 0.375rem 1.75rem 0.375rem 0.75rem; line-height: 1.5; color: #495057; vertical-align: middle; background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right 0.75rem center; background-size: 8px 10px; border: 1px solid #ced4da; border-radius: 0.25rem; -webkit-appearance: none; -moz-appearance: none; appearance: none; } .custom-select:focus { border-color: #80bdff; outline: 0; box-shadow: 0 0 0 0.2rem rgba(128, 189, 255, 0.5); } .custom-select:focus::-ms-value { color: #495057; background-color: #fff; } .custom-select[multiple], .custom-select[size]:not([size="1"]) { height: auto; padding-right: 0.75rem; background-image: none; } .custom-select:disabled { color: #6c757d; background-color: #e9ecef; } .custom-select::-ms-expand { opacity: 0; } .custom-select-sm { height: calc(1.8125rem + 2px); padding-top: 0.375rem; padding-bottom: 0.375rem; font-size: 75%; } .custom-select-lg { height: calc(2.875rem + 2px); padding-top: 0.375rem; padding-bottom: 0.375rem; font-size: 125%; } .custom-file { position: relative; display: inline-block; width: 100%; height: calc(2.25rem + 2px); margin-bottom: 0; } .custom-file-input { position: relative; z-index: 2; width: 100%; height: calc(2.25rem + 2px); margin: 0; opacity: 0; } .custom-file-input:focus ~ .custom-file-label { border-color: #80bdff; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-file-input:focus ~ .custom-file-label::after { border-color: #80bdff; } .custom-file-input:disabled ~ .custom-file-label { background-color: #e9ecef; } .custom-file-input:lang(en) ~ .custom-file-label::after { content: "Browse"; } .custom-file-label { position: absolute; top: 0; right: 0; left: 0; z-index: 1; height: calc(2.25rem + 2px); padding: 0.375rem 0.75rem; line-height: 1.5; color: #495057; background-color: #fff; border: 1px solid #ced4da; border-radius: 0.25rem; } .custom-file-label::after { position: absolute; top: 0; right: 0; bottom: 0; z-index: 3; display: block; height: 2.25rem; padding: 0.375rem 0.75rem; line-height: 1.5; color: #495057; content: "Browse"; background-color: #e9ecef; border-left: 1px solid #ced4da; border-radius: 0 0.25rem 0.25rem 0; } .custom-range { width: 100%; padding-left: 0; background-color: transparent; -webkit-appearance: none; -moz-appearance: none; appearance: none; } .custom-range:focus { outline: none; } .custom-range:focus::-webkit-slider-thumb { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-range:focus::-moz-range-thumb { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-range:focus::-ms-thumb { box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .custom-range::-moz-focus-outer { border: 0; } .custom-range::-webkit-slider-thumb { width: 1rem; height: 1rem; margin-top: -0.25rem; background-color: #007bff; border: 0; border-radius: 1rem; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -webkit-appearance: none; appearance: none; } @media screen and (prefers-reduced-motion: reduce) { .custom-range::-webkit-slider-thumb { transition: none; } } .custom-range::-webkit-slider-thumb:active { background-color: #b3d7ff; } .custom-range::-webkit-slider-runnable-track { width: 100%; height: 0.5rem; color: transparent; cursor: pointer; background-color: #dee2e6; border-color: transparent; border-radius: 1rem; } .custom-range::-moz-range-thumb { width: 1rem; height: 1rem; background-color: #007bff; border: 0; border-radius: 1rem; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -moz-appearance: none; appearance: none; } @media screen and (prefers-reduced-motion: reduce) { .custom-range::-moz-range-thumb { transition: none; } } .custom-range::-moz-range-thumb:active { background-color: #b3d7ff; } .custom-range::-moz-range-track { width: 100%; height: 0.5rem; color: transparent; cursor: pointer; background-color: #dee2e6; border-color: transparent; border-radius: 1rem; } .custom-range::-ms-thumb { width: 1rem; height: 1rem; margin-top: 0; margin-right: 0.2rem; margin-left: 0.2rem; background-color: #007bff; border: 0; border-radius: 1rem; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; appearance: none; } @media screen and (prefers-reduced-motion: reduce) { .custom-range::-ms-thumb { transition: none; } } .custom-range::-ms-thumb:active { background-color: #b3d7ff; } .custom-range::-ms-track { width: 100%; height: 0.5rem; color: transparent; cursor: pointer; background-color: transparent; border-color: transparent; border-width: 0.5rem; } .custom-range::-ms-fill-lower { background-color: #dee2e6; border-radius: 1rem; } .custom-range::-ms-fill-upper { margin-right: 15px; background-color: #dee2e6; border-radius: 1rem; } .custom-control-label::before, .custom-file-label, .custom-select { transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media screen and (prefers-reduced-motion: reduce) { .custom-control-label::before, .custom-file-label, .custom-select { transition: none; } } .nav { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; padding-left: 0; margin-bottom: 0; list-style: none; } .nav-link { display: block; padding: 0.5rem 1rem; } .nav-link:hover, .nav-link:focus { text-decoration: none; } .nav-link.disabled { color: #6c757d; } .nav-tabs { border-bottom: 1px solid #dee2e6; } .nav-tabs .nav-item { margin-bottom: -1px; } .nav-tabs .nav-link { border: 1px solid transparent; border-top-left-radius: 0.25rem; border-top-right-radius: 0.25rem; } .nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { border-color: #e9ecef #e9ecef #dee2e6; } .nav-tabs .nav-link.disabled { color: #6c757d; background-color: transparent; border-color: transparent; } .nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link { color: #495057; background-color: #fff; border-color: #dee2e6 #dee2e6 #fff; } .nav-tabs .dropdown-menu { margin-top: -1px; border-top-left-radius: 0; border-top-right-radius: 0; } .nav-pills .nav-link { border-radius: 0.25rem; } .nav-pills .nav-link.active, .nav-pills .show > .nav-link { color: #fff; background-color: #007bff; } .nav-fill .nav-item { -ms-flex: 1 1 auto; flex: 1 1 auto; text-align: center; } .nav-justified .nav-item { -ms-flex-preferred-size: 0; flex-basis: 0; -ms-flex-positive: 1; flex-grow: 1; text-align: center; } .tab-content > .tab-pane { display: none; } .tab-content > .active { display: block; } .navbar { position: relative; display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-align: center; align-items: center; -ms-flex-pack: justify; justify-content: space-between; padding: 0.5rem 1rem; } .navbar > .container, .navbar > .container-fluid { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-align: center; align-items: center; -ms-flex-pack: justify; justify-content: space-between; } .navbar-brand { display: inline-block; padding-top: 0.3125rem; padding-bottom: 0.3125rem; margin-right: 1rem; font-size: 1.25rem; line-height: inherit; white-space: nowrap; } .navbar-brand:hover, .navbar-brand:focus { text-decoration: none; } .navbar-nav { display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; padding-left: 0; margin-bottom: 0; list-style: none; } .navbar-nav .nav-link { padding-right: 0; padding-left: 0; } .navbar-nav .dropdown-menu { position: static; float: none; } .navbar-text { display: inline-block; padding-top: 0.5rem; padding-bottom: 0.5rem; } .navbar-collapse { -ms-flex-preferred-size: 100%; flex-basis: 100%; -ms-flex-positive: 1; flex-grow: 1; -ms-flex-align: center; align-items: center; } .navbar-toggler { padding: 0.25rem 0.75rem; font-size: 1.25rem; line-height: 1; background-color: transparent; border: 1px solid transparent; border-radius: 0.25rem; } .navbar-toggler:hover, .navbar-toggler:focus { text-decoration: none; } .navbar-toggler:not(:disabled):not(.disabled) { cursor: pointer; } .navbar-toggler-icon { display: inline-block; width: 1.5em; height: 1.5em; vertical-align: middle; content: ""; background: no-repeat center center; background-size: 100% 100%; } @media (max-width: 575.98px) { .navbar-expand-sm > .container, .navbar-expand-sm > .container-fluid { padding-right: 0; padding-left: 0; } } @media (min-width: 576px) { .navbar-expand-sm { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand-sm .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand-sm .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand-sm .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand-sm > .container, .navbar-expand-sm > .container-fluid { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand-sm .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand-sm .navbar-toggler { display: none; } } @media (max-width: 767.98px) { .navbar-expand-md > .container, .navbar-expand-md > .container-fluid { padding-right: 0; padding-left: 0; } } @media (min-width: 768px) { .navbar-expand-md { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand-md .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand-md .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand-md .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand-md > .container, .navbar-expand-md > .container-fluid { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand-md .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand-md .navbar-toggler { display: none; } } @media (max-width: 991.98px) { .navbar-expand-lg > .container, .navbar-expand-lg > .container-fluid { padding-right: 0; padding-left: 0; } } @media (min-width: 992px) { .navbar-expand-lg { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand-lg .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand-lg .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand-lg .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand-lg > .container, .navbar-expand-lg > .container-fluid { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand-lg .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand-lg .navbar-toggler { display: none; } } @media (max-width: 1199.98px) { .navbar-expand-xl > .container, .navbar-expand-xl > .container-fluid { padding-right: 0; padding-left: 0; } } @media (min-width: 1200px) { .navbar-expand-xl { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand-xl .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand-xl .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand-xl .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand-xl > .container, .navbar-expand-xl > .container-fluid { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand-xl .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand-xl .navbar-toggler { display: none; } } .navbar-expand { -ms-flex-flow: row nowrap; flex-flow: row nowrap; -ms-flex-pack: start; justify-content: flex-start; } .navbar-expand > .container, .navbar-expand > .container-fluid { padding-right: 0; padding-left: 0; } .navbar-expand .navbar-nav { -ms-flex-direction: row; flex-direction: row; } .navbar-expand .navbar-nav .dropdown-menu { position: absolute; } .navbar-expand .navbar-nav .nav-link { padding-right: 0.5rem; padding-left: 0.5rem; } .navbar-expand > .container, .navbar-expand > .container-fluid { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } .navbar-expand .navbar-collapse { display: -ms-flexbox !important; display: flex !important; -ms-flex-preferred-size: auto; flex-basis: auto; } .navbar-expand .navbar-toggler { display: none; } .navbar-light .navbar-brand { color: rgba(0, 0, 0, 0.9); } .navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { color: rgba(0, 0, 0, 0.9); } .navbar-light .navbar-nav .nav-link { color: rgba(0, 0, 0, 0.5); } .navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { color: rgba(0, 0, 0, 0.7); } .navbar-light .navbar-nav .nav-link.disabled { color: rgba(0, 0, 0, 0.3); } .navbar-light .navbar-nav .show > .nav-link, .navbar-light .navbar-nav .active > .nav-link, .navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .nav-link.active { color: rgba(0, 0, 0, 0.9); } .navbar-light .navbar-toggler { color: rgba(0, 0, 0, 0.5); border-color: rgba(0, 0, 0, 0.1); } .navbar-light .navbar-toggler-icon { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); } .navbar-light .navbar-text { color: rgba(0, 0, 0, 0.5); } .navbar-light .navbar-text a { color: rgba(0, 0, 0, 0.9); } .navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus { color: rgba(0, 0, 0, 0.9); } .navbar-dark .navbar-brand { color: #fff; } .navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { color: #fff; } .navbar-dark .navbar-nav .nav-link { color: rgba(255, 255, 255, 0.5); } .navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { color: rgba(255, 255, 255, 0.75); } .navbar-dark .navbar-nav .nav-link.disabled { color: rgba(255, 255, 255, 0.25); } .navbar-dark .navbar-nav .show > .nav-link, .navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .nav-link.active { color: #fff; } .navbar-dark .navbar-toggler { color: rgba(255, 255, 255, 0.5); border-color: rgba(255, 255, 255, 0.1); } .navbar-dark .navbar-toggler-icon { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); } .navbar-dark .navbar-text { color: rgba(255, 255, 255, 0.5); } .navbar-dark .navbar-text a { color: #fff; } .navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus { color: #fff; } .card { position: relative; display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; min-width: 0; word-wrap: break-word; background-color: #fff; background-clip: border-box; border: 1px solid rgba(0, 0, 0, 0.125); border-radius: 0.25rem; } .card > hr { margin-right: 0; margin-left: 0; } .card > .list-group:first-child .list-group-item:first-child { border-top-left-radius: 0.25rem; border-top-right-radius: 0.25rem; } .card > .list-group:last-child .list-group-item:last-child { border-bottom-right-radius: 0.25rem; border-bottom-left-radius: 0.25rem; } .card-body { -ms-flex: 1 1 auto; flex: 1 1 auto; padding: 1.25rem; } .card-title { margin-bottom: 0.75rem; } .card-subtitle { margin-top: -0.375rem; margin-bottom: 0; } .card-text:last-child { margin-bottom: 0; } .card-link:hover { text-decoration: none; } .card-link + .card-link { margin-left: 1.25rem; } .card-header { padding: 0.75rem 1.25rem; margin-bottom: 0; background-color: rgba(0, 0, 0, 0.03); border-bottom: 1px solid rgba(0, 0, 0, 0.125); } .card-header:first-child { border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; } .card-header + .list-group .list-group-item:first-child { border-top: 0; } .card-footer { padding: 0.75rem 1.25rem; background-color: rgba(0, 0, 0, 0.03); border-top: 1px solid rgba(0, 0, 0, 0.125); } .card-footer:last-child { border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); } .card-header-tabs { margin-right: -0.625rem; margin-bottom: -0.75rem; margin-left: -0.625rem; border-bottom: 0; } .card-header-pills { margin-right: -0.625rem; margin-left: -0.625rem; } .card-img-overlay { position: absolute; top: 0; right: 0; bottom: 0; left: 0; padding: 1.25rem; } .card-img { width: 100%; border-radius: calc(0.25rem - 1px); } .card-img-top { width: 100%; border-top-left-radius: calc(0.25rem - 1px); border-top-right-radius: calc(0.25rem - 1px); } .card-img-bottom { width: 100%; border-bottom-right-radius: calc(0.25rem - 1px); border-bottom-left-radius: calc(0.25rem - 1px); } .card-deck { display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; } .card-deck .card { margin-bottom: 15px; } @media (min-width: 576px) { .card-deck { -ms-flex-flow: row wrap; flex-flow: row wrap; margin-right: -15px; margin-left: -15px; } .card-deck .card { display: -ms-flexbox; display: flex; -ms-flex: 1 0 0%; flex: 1 0 0%; -ms-flex-direction: column; flex-direction: column; margin-right: 15px; margin-bottom: 0; margin-left: 15px; } } .card-group { display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; } .card-group > .card { margin-bottom: 15px; } @media (min-width: 576px) { .card-group { -ms-flex-flow: row wrap; flex-flow: row wrap; } .card-group > .card { -ms-flex: 1 0 0%; flex: 1 0 0%; margin-bottom: 0; } .card-group > .card + .card { margin-left: 0; border-left: 0; } .card-group > .card:first-child { border-top-right-radius: 0; border-bottom-right-radius: 0; } .card-group > .card:first-child .card-img-top, .card-group > .card:first-child .card-header { border-top-right-radius: 0; } .card-group > .card:first-child .card-img-bottom, .card-group > .card:first-child .card-footer { border-bottom-right-radius: 0; } .card-group > .card:last-child { border-top-left-radius: 0; border-bottom-left-radius: 0; } .card-group > .card:last-child .card-img-top, .card-group > .card:last-child .card-header { border-top-left-radius: 0; } .card-group > .card:last-child .card-img-bottom, .card-group > .card:last-child .card-footer { border-bottom-left-radius: 0; } .card-group > .card:only-child { border-radius: 0.25rem; } .card-group > .card:only-child .card-img-top, .card-group > .card:only-child .card-header { border-top-left-radius: 0.25rem; border-top-right-radius: 0.25rem; } .card-group > .card:only-child .card-img-bottom, .card-group > .card:only-child .card-footer { border-bottom-right-radius: 0.25rem; border-bottom-left-radius: 0.25rem; } .card-group > .card:not(:first-child):not(:last-child):not(:only-child) { border-radius: 0; } .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-top, .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom, .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-header, .card-group > .card:not(:first-child):not(:last-child):not(:only-child) .card-footer { border-radius: 0; } } .card-columns .card { margin-bottom: 0.75rem; } @media (min-width: 576px) { .card-columns { -webkit-column-count: 3; -moz-column-count: 3; column-count: 3; -webkit-column-gap: 1.25rem; -moz-column-gap: 1.25rem; column-gap: 1.25rem; orphans: 1; widows: 1; } .card-columns .card { display: inline-block; width: 100%; } } .accordion .card:not(:first-of-type):not(:last-of-type) { border-bottom: 0; border-radius: 0; } .accordion .card:not(:first-of-type) .card-header:first-child { border-radius: 0; } .accordion .card:first-of-type { border-bottom: 0; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .accordion .card:last-of-type { border-top-left-radius: 0; border-top-right-radius: 0; } .breadcrumb { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; padding: 0.75rem 1rem; margin-bottom: 1rem; list-style: none; background-color: #e9ecef; border-radius: 0.25rem; } .breadcrumb-item + .breadcrumb-item { padding-left: 0.5rem; } .breadcrumb-item + .breadcrumb-item::before { display: inline-block; padding-right: 0.5rem; color: #6c757d; content: "/"; } .breadcrumb-item + .breadcrumb-item:hover::before { text-decoration: underline; } .breadcrumb-item + .breadcrumb-item:hover::before { text-decoration: none; } .breadcrumb-item.active { color: #6c757d; } .pagination { display: -ms-flexbox; display: flex; padding-left: 0; list-style: none; border-radius: 0.25rem; } .page-link { position: relative; display: block; padding: 0.5rem 0.75rem; margin-left: -1px; line-height: 1.25; color: #007bff; background-color: #fff; border: 1px solid #dee2e6; } .page-link:hover { z-index: 2; color: #0056b3; text-decoration: none; background-color: #e9ecef; border-color: #dee2e6; } .page-link:focus { z-index: 2; outline: 0; box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } .page-link:not(:disabled):not(.disabled) { cursor: pointer; } .page-item:first-child .page-link { margin-left: 0; border-top-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem; } .page-item:last-child .page-link { border-top-right-radius: 0.25rem; border-bottom-right-radius: 0.25rem; } .page-item.active .page-link { z-index: 1; color: #fff; background-color: #007bff; border-color: #007bff; } .page-item.disabled .page-link { color: #6c757d; pointer-events: none; cursor: auto; background-color: #fff; border-color: #dee2e6; } .pagination-lg .page-link { padding: 0.75rem 1.5rem; font-size: 1.25rem; line-height: 1.5; } .pagination-lg .page-item:first-child .page-link { border-top-left-radius: 0.3rem; border-bottom-left-radius: 0.3rem; } .pagination-lg .page-item:last-child .page-link { border-top-right-radius: 0.3rem; border-bottom-right-radius: 0.3rem; } .pagination-sm .page-link { padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.5; } .pagination-sm .page-item:first-child .page-link { border-top-left-radius: 0.2rem; border-bottom-left-radius: 0.2rem; } .pagination-sm .page-item:last-child .page-link { border-top-right-radius: 0.2rem; border-bottom-right-radius: 0.2rem; } .badge { display: inline-block; padding: 0.25em 0.4em; font-size: 75%; font-weight: 700; line-height: 1; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: 0.25rem; } .badge:empty { display: none; } .btn .badge { position: relative; top: -1px; } .badge-pill { padding-right: 0.6em; padding-left: 0.6em; border-radius: 10rem; } .badge-primary { color: #fff; background-color: #007bff; } .badge-primary[href]:hover, .badge-primary[href]:focus { color: #fff; text-decoration: none; background-color: #0062cc; } .badge-secondary { color: #fff; background-color: #6c757d; } .badge-secondary[href]:hover, .badge-secondary[href]:focus { color: #fff; text-decoration: none; background-color: #545b62; } .badge-success { color: #fff; background-color: #28a745; } .badge-success[href]:hover, .badge-success[href]:focus { color: #fff; text-decoration: none; background-color: #1e7e34; } .badge-info { color: #fff; background-color: #17a2b8; } .badge-info[href]:hover, .badge-info[href]:focus { color: #fff; text-decoration: none; background-color: #117a8b; } .badge-warning { color: #212529; background-color: #ffc107; } .badge-warning[href]:hover, .badge-warning[href]:focus { color: #212529; text-decoration: none; background-color: #d39e00; } .badge-danger { color: #fff; background-color: #dc3545; } .badge-danger[href]:hover, .badge-danger[href]:focus { color: #fff; text-decoration: none; background-color: #bd2130; } .badge-light { color: #212529; background-color: #f8f9fa; } .badge-light[href]:hover, .badge-light[href]:focus { color: #212529; text-decoration: none; background-color: #dae0e5; } .badge-dark { color: #fff; background-color: #343a40; } .badge-dark[href]:hover, .badge-dark[href]:focus { color: #fff; text-decoration: none; background-color: #1d2124; } .jumbotron { padding: 2rem 1rem; margin-bottom: 2rem; background-color: #e9ecef; border-radius: 0.3rem; } @media (min-width: 576px) { .jumbotron { padding: 4rem 2rem; } } .jumbotron-fluid { padding-right: 0; padding-left: 0; border-radius: 0; } .alert { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 0.25rem; } .alert-heading { color: inherit; } .alert-link { font-weight: 700; } .alert-dismissible { padding-right: 4rem; } .alert-dismissible .close { position: absolute; top: 0; right: 0; padding: 0.75rem 1.25rem; color: inherit; } .alert-primary { color: #004085; background-color: #cce5ff; border-color: #b8daff; } .alert-primary hr { border-top-color: #9fcdff; } .alert-primary .alert-link { color: #002752; } .alert-secondary { color: #383d41; background-color: #e2e3e5; border-color: #d6d8db; } .alert-secondary hr { border-top-color: #c8cbcf; } .alert-secondary .alert-link { color: #202326; } .alert-success { color: #155724; background-color: #d4edda; border-color: #c3e6cb; } .alert-success hr { border-top-color: #b1dfbb; } .alert-success .alert-link { color: #0b2e13; } .alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; } .alert-info hr { border-top-color: #abdde5; } .alert-info .alert-link { color: #062c33; } .alert-warning { color: #856404; background-color: #fff3cd; border-color: #ffeeba; } .alert-warning hr { border-top-color: #ffe8a1; } .alert-warning .alert-link { color: #533f03; } .alert-danger { color: #721c24; background-color: #f8d7da; border-color: #f5c6cb; } .alert-danger hr { border-top-color: #f1b0b7; } .alert-danger .alert-link { color: #491217; } .alert-light { color: #818182; background-color: #fefefe; border-color: #fdfdfe; } .alert-light hr { border-top-color: #ececf6; } .alert-light .alert-link { color: #686868; } .alert-dark { color: #1b1e21; background-color: #d6d8d9; border-color: #c6c8ca; } .alert-dark hr { border-top-color: #b9bbbe; } .alert-dark .alert-link { color: #040505; } @-webkit-keyframes progress-bar-stripes { from { background-position: 1rem 0; } to { background-position: 0 0; } } @keyframes progress-bar-stripes { from { background-position: 1rem 0; } to { background-position: 0 0; } } .progress { display: -ms-flexbox; display: flex; height: 1rem; overflow: hidden; font-size: 0.75rem; background-color: #e9ecef; border-radius: 0.25rem; } .progress-bar { display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; -ms-flex-pack: center; justify-content: center; color: #fff; text-align: center; white-space: nowrap; background-color: #007bff; transition: width 0.6s ease; } @media screen and (prefers-reduced-motion: reduce) { .progress-bar { transition: none; } } .progress-bar-striped { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-size: 1rem 1rem; } .progress-bar-animated { -webkit-animation: progress-bar-stripes 1s linear infinite; animation: progress-bar-stripes 1s linear infinite; } .media { display: -ms-flexbox; display: flex; -ms-flex-align: start; align-items: flex-start; } .media-body { -ms-flex: 1; flex: 1; } .list-group { display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; padding-left: 0; margin-bottom: 0; } .list-group-item-action { width: 100%; color: #495057; text-align: inherit; } .list-group-item-action:hover, .list-group-item-action:focus { color: #495057; text-decoration: none; background-color: #f8f9fa; } .list-group-item-action:active { color: #212529; background-color: #e9ecef; } .list-group-item { position: relative; display: block; padding: 0.75rem 1.25rem; margin-bottom: -1px; background-color: #fff; border: 1px solid rgba(0, 0, 0, 0.125); } .list-group-item:first-child { border-top-left-radius: 0.25rem; border-top-right-radius: 0.25rem; } .list-group-item:last-child { margin-bottom: 0; border-bottom-right-radius: 0.25rem; border-bottom-left-radius: 0.25rem; } .list-group-item:hover, .list-group-item:focus { z-index: 1; text-decoration: none; } .list-group-item.disabled, .list-group-item:disabled { color: #6c757d; background-color: #fff; } .list-group-item.active { z-index: 2; color: #fff; background-color: #007bff; border-color: #007bff; } .list-group-flush .list-group-item { border-right: 0; border-left: 0; border-radius: 0; } .list-group-flush:first-child .list-group-item:first-child { border-top: 0; } .list-group-flush:last-child .list-group-item:last-child { border-bottom: 0; } .list-group-item-primary { color: #004085; background-color: #b8daff; } .list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { color: #004085; background-color: #9fcdff; } .list-group-item-primary.list-group-item-action.active { color: #fff; background-color: #004085; border-color: #004085; } .list-group-item-secondary { color: #383d41; background-color: #d6d8db; } .list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { color: #383d41; background-color: #c8cbcf; } .list-group-item-secondary.list-group-item-action.active { color: #fff; background-color: #383d41; border-color: #383d41; } .list-group-item-success { color: #155724; background-color: #c3e6cb; } .list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { color: #155724; background-color: #b1dfbb; } .list-group-item-success.list-group-item-action.active { color: #fff; background-color: #155724; border-color: #155724; } .list-group-item-info { color: #0c5460; background-color: #bee5eb; } .list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { color: #0c5460; background-color: #abdde5; } .list-group-item-info.list-group-item-action.active { color: #fff; background-color: #0c5460; border-color: #0c5460; } .list-group-item-warning { color: #856404; background-color: #ffeeba; } .list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { color: #856404; background-color: #ffe8a1; } .list-group-item-warning.list-group-item-action.active { color: #fff; background-color: #856404; border-color: #856404; } .list-group-item-danger { color: #721c24; background-color: #f5c6cb; } .list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { color: #721c24; background-color: #f1b0b7; } .list-group-item-danger.list-group-item-action.active { color: #fff; background-color: #721c24; border-color: #721c24; } .list-group-item-light { color: #818182; background-color: #fdfdfe; } .list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { color: #818182; background-color: #ececf6; } .list-group-item-light.list-group-item-action.active { color: #fff; background-color: #818182; border-color: #818182; } .list-group-item-dark { color: #1b1e21; background-color: #c6c8ca; } .list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { color: #1b1e21; background-color: #b9bbbe; } .list-group-item-dark.list-group-item-action.active { color: #fff; background-color: #1b1e21; border-color: #1b1e21; } .close { float: right; font-size: 1.5rem; font-weight: 700; line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; opacity: .5; } .close:not(:disabled):not(.disabled) { cursor: pointer; } .close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus { color: #000; text-decoration: none; opacity: .75; } button.close { padding: 0; background-color: transparent; border: 0; -webkit-appearance: none; } .modal-open { overflow: hidden; } .modal-open .modal { overflow-x: hidden; overflow-y: auto; } .modal { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1050; display: none; overflow: hidden; outline: 0; } .modal-dialog { position: relative; width: auto; margin: 0.5rem; pointer-events: none; } .modal.fade .modal-dialog { transition: -webkit-transform 0.3s ease-out; transition: transform 0.3s ease-out; transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out; -webkit-transform: translate(0, -25%); transform: translate(0, -25%); } @media screen and (prefers-reduced-motion: reduce) { .modal.fade .modal-dialog { transition: none; } } .modal.show .modal-dialog { -webkit-transform: translate(0, 0); transform: translate(0, 0); } .modal-dialog-centered { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; min-height: calc(100% - (0.5rem * 2)); } .modal-dialog-centered::before { display: block; height: calc(100vh - (0.5rem * 2)); content: ""; } .modal-content { position: relative; display: -ms-flexbox; display: flex; -ms-flex-direction: column; flex-direction: column; width: 100%; pointer-events: auto; background-color: #fff; background-clip: padding-box; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 0.3rem; outline: 0; } .modal-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1040; background-color: #000; } .modal-backdrop.fade { opacity: 0; } .modal-backdrop.show { opacity: 0.5; } .modal-header { display: -ms-flexbox; display: flex; -ms-flex-align: start; align-items: flex-start; -ms-flex-pack: justify; justify-content: space-between; padding: 1rem; border-bottom: 1px solid #e9ecef; border-top-left-radius: 0.3rem; border-top-right-radius: 0.3rem; } .modal-header .close { padding: 1rem; margin: -1rem -1rem -1rem auto; } .modal-title { margin-bottom: 0; line-height: 1.5; } .modal-body { position: relative; -ms-flex: 1 1 auto; flex: 1 1 auto; padding: 1rem; } .modal-footer { display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; -ms-flex-pack: end; justify-content: flex-end; padding: 1rem; border-top: 1px solid #e9ecef; } .modal-footer > :not(:first-child) { margin-left: .25rem; } .modal-footer > :not(:last-child) { margin-right: .25rem; } .modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; } @media (min-width: 576px) { .modal-dialog { max-width: 500px; margin: 1.75rem auto; } .modal-dialog-centered { min-height: calc(100% - (1.75rem * 2)); } .modal-dialog-centered::before { height: calc(100vh - (1.75rem * 2)); } .modal-sm { max-width: 300px; } } @media (min-width: 992px) { .modal-lg { max-width: 800px; } } .tooltip { position: absolute; z-index: 1070; display: block; margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-style: normal; font-weight: 400; line-height: 1.5; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; white-space: normal; line-break: auto; font-size: 0.875rem; word-wrap: break-word; opacity: 0; } .tooltip.show { opacity: 0.9; } .tooltip .arrow { position: absolute; display: block; width: 0.8rem; height: 0.4rem; } .tooltip .arrow::before { position: absolute; content: ""; border-color: transparent; border-style: solid; } .bs-tooltip-top, .bs-tooltip-auto[x-placement^="top"] { padding: 0.4rem 0; } .bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^="top"] .arrow { bottom: 0; } .bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^="top"] .arrow::before { top: 0; border-width: 0.4rem 0.4rem 0; border-top-color: #000; } .bs-tooltip-right, .bs-tooltip-auto[x-placement^="right"] { padding: 0 0.4rem; } .bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^="right"] .arrow { left: 0; width: 0.4rem; height: 0.8rem; } .bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^="right"] .arrow::before { right: 0; border-width: 0.4rem 0.4rem 0.4rem 0; border-right-color: #000; } .bs-tooltip-bottom, .bs-tooltip-auto[x-placement^="bottom"] { padding: 0.4rem 0; } .bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^="bottom"] .arrow { top: 0; } .bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^="bottom"] .arrow::before { bottom: 0; border-width: 0 0.4rem 0.4rem; border-bottom-color: #000; } .bs-tooltip-left, .bs-tooltip-auto[x-placement^="left"] { padding: 0 0.4rem; } .bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^="left"] .arrow { right: 0; width: 0.4rem; height: 0.8rem; } .bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^="left"] .arrow::before { left: 0; border-width: 0.4rem 0 0.4rem 0.4rem; border-left-color: #000; } .tooltip-inner { max-width: 200px; padding: 0.25rem 0.5rem; color: #fff; text-align: center; background-color: #000; border-radius: 0.25rem; } .popover { position: absolute; top: 0; left: 0; z-index: 1060; display: block; max-width: 276px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-style: normal; font-weight: 400; line-height: 1.5; text-align: left; text-align: start; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; word-break: normal; word-spacing: normal; white-space: normal; line-break: auto; font-size: 0.875rem; word-wrap: break-word; background-color: #fff; background-clip: padding-box; border: 1px solid rgba(0, 0, 0, 0.2); border-radius: 0.3rem; } .popover .arrow { position: absolute; display: block; width: 1rem; height: 0.5rem; margin: 0 0.3rem; } .popover .arrow::before, .popover .arrow::after { position: absolute; display: block; content: ""; border-color: transparent; border-style: solid; } .bs-popover-top, .bs-popover-auto[x-placement^="top"] { margin-bottom: 0.5rem; } .bs-popover-top .arrow, .bs-popover-auto[x-placement^="top"] .arrow { bottom: calc((0.5rem + 1px) * -1); } .bs-popover-top .arrow::before, .bs-popover-auto[x-placement^="top"] .arrow::before, .bs-popover-top .arrow::after, .bs-popover-auto[x-placement^="top"] .arrow::after { border-width: 0.5rem 0.5rem 0; } .bs-popover-top .arrow::before, .bs-popover-auto[x-placement^="top"] .arrow::before { bottom: 0; border-top-color: rgba(0, 0, 0, 0.25); } .bs-popover-top .arrow::after, .bs-popover-auto[x-placement^="top"] .arrow::after { bottom: 1px; border-top-color: #fff; } .bs-popover-right, .bs-popover-auto[x-placement^="right"] { margin-left: 0.5rem; } .bs-popover-right .arrow, .bs-popover-auto[x-placement^="right"] .arrow { left: calc((0.5rem + 1px) * -1); width: 0.5rem; height: 1rem; margin: 0.3rem 0; } .bs-popover-right .arrow::before, .bs-popover-auto[x-placement^="right"] .arrow::before, .bs-popover-right .arrow::after, .bs-popover-auto[x-placement^="right"] .arrow::after { border-width: 0.5rem 0.5rem 0.5rem 0; } .bs-popover-right .arrow::before, .bs-popover-auto[x-placement^="right"] .arrow::before { left: 0; border-right-color: rgba(0, 0, 0, 0.25); } .bs-popover-right .arrow::after, .bs-popover-auto[x-placement^="right"] .arrow::after { left: 1px; border-right-color: #fff; } .bs-popover-bottom, .bs-popover-auto[x-placement^="bottom"] { margin-top: 0.5rem; } .bs-popover-bottom .arrow, .bs-popover-auto[x-placement^="bottom"] .arrow { top: calc((0.5rem + 1px) * -1); } .bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^="bottom"] .arrow::before, .bs-popover-bottom .arrow::after, .bs-popover-auto[x-placement^="bottom"] .arrow::after { border-width: 0 0.5rem 0.5rem 0.5rem; } .bs-popover-bottom .arrow::before, .bs-popover-auto[x-placement^="bottom"] .arrow::before { top: 0; border-bottom-color: rgba(0, 0, 0, 0.25); } .bs-popover-bottom .arrow::after, .bs-popover-auto[x-placement^="bottom"] .arrow::after { top: 1px; border-bottom-color: #fff; } .bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { position: absolute; top: 0; left: 50%; display: block; width: 1rem; margin-left: -0.5rem; content: ""; border-bottom: 1px solid #f7f7f7; } .bs-popover-left, .bs-popover-auto[x-placement^="left"] { margin-right: 0.5rem; } .bs-popover-left .arrow, .bs-popover-auto[x-placement^="left"] .arrow { right: calc((0.5rem + 1px) * -1); width: 0.5rem; height: 1rem; margin: 0.3rem 0; } .bs-popover-left .arrow::before, .bs-popover-auto[x-placement^="left"] .arrow::before, .bs-popover-left .arrow::after, .bs-popover-auto[x-placement^="left"] .arrow::after { border-width: 0.5rem 0 0.5rem 0.5rem; } .bs-popover-left .arrow::before, .bs-popover-auto[x-placement^="left"] .arrow::before { right: 0; border-left-color: rgba(0, 0, 0, 0.25); } .bs-popover-left .arrow::after, .bs-popover-auto[x-placement^="left"] .arrow::after { right: 1px; border-left-color: #fff; } .popover-header { padding: 0.5rem 0.75rem; margin-bottom: 0; font-size: 1rem; color: inherit; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-top-left-radius: calc(0.3rem - 1px); border-top-right-radius: calc(0.3rem - 1px); } .popover-header:empty { display: none; } .popover-body { padding: 0.5rem 0.75rem; color: #212529; } .carousel { position: relative; } .carousel-inner { position: relative; width: 100%; overflow: hidden; } .carousel-item { position: relative; display: none; -ms-flex-align: center; align-items: center; width: 100%; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-perspective: 1000px; perspective: 1000px; } .carousel-item.active, .carousel-item-next, .carousel-item-prev { display: block; transition: -webkit-transform 0.6s ease; transition: transform 0.6s ease; transition: transform 0.6s ease, -webkit-transform 0.6s ease; } @media screen and (prefers-reduced-motion: reduce) { .carousel-item.active, .carousel-item-next, .carousel-item-prev { transition: none; } } .carousel-item-next, .carousel-item-prev { position: absolute; top: 0; } .carousel-item-next.carousel-item-left, .carousel-item-prev.carousel-item-right { -webkit-transform: translateX(0); transform: translateX(0); } @supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) { .carousel-item-next.carousel-item-left, .carousel-item-prev.carousel-item-right { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .carousel-item-next, .active.carousel-item-right { -webkit-transform: translateX(100%); transform: translateX(100%); } @supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) { .carousel-item-next, .active.carousel-item-right { -webkit-transform: translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0); } } .carousel-item-prev, .active.carousel-item-left { -webkit-transform: translateX(-100%); transform: translateX(-100%); } @supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) { .carousel-item-prev, .active.carousel-item-left { -webkit-transform: translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0); } } .carousel-fade .carousel-item { opacity: 0; transition-duration: .6s; transition-property: opacity; } .carousel-fade .carousel-item.active, .carousel-fade .carousel-item-next.carousel-item-left, .carousel-fade .carousel-item-prev.carousel-item-right { opacity: 1; } .carousel-fade .active.carousel-item-left, .carousel-fade .active.carousel-item-right { opacity: 0; } .carousel-fade .carousel-item-next, .carousel-fade .carousel-item-prev, .carousel-fade .carousel-item.active, .carousel-fade .active.carousel-item-left, .carousel-fade .active.carousel-item-prev { -webkit-transform: translateX(0); transform: translateX(0); } @supports ((-webkit-transform-style: preserve-3d) or (transform-style: preserve-3d)) { .carousel-fade .carousel-item-next, .carousel-fade .carousel-item-prev, .carousel-fade .carousel-item.active, .carousel-fade .active.carousel-item-left, .carousel-fade .active.carousel-item-prev { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .carousel-control-prev, .carousel-control-next { position: absolute; top: 0; bottom: 0; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; -ms-flex-pack: center; justify-content: center; width: 15%; color: #fff; text-align: center; opacity: 0.5; } .carousel-control-prev:hover, .carousel-control-prev:focus, .carousel-control-next:hover, .carousel-control-next:focus { color: #fff; text-decoration: none; outline: 0; opacity: .9; } .carousel-control-prev { left: 0; } .carousel-control-next { right: 0; } .carousel-control-prev-icon, .carousel-control-next-icon { display: inline-block; width: 20px; height: 20px; background: transparent no-repeat center center; background-size: 100% 100%; } .carousel-control-prev-icon { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E"); } .carousel-control-next-icon { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E"); } .carousel-indicators { position: absolute; right: 0; bottom: 10px; left: 0; z-index: 15; display: -ms-flexbox; display: flex; -ms-flex-pack: center; justify-content: center; padding-left: 0; margin-right: 15%; margin-left: 15%; list-style: none; } .carousel-indicators li { position: relative; -ms-flex: 0 1 auto; flex: 0 1 auto; width: 30px; height: 3px; margin-right: 3px; margin-left: 3px; text-indent: -999px; cursor: pointer; background-color: rgba(255, 255, 255, 0.5); } .carousel-indicators li::before { position: absolute; top: -10px; left: 0; display: inline-block; width: 100%; height: 10px; content: ""; } .carousel-indicators li::after { position: absolute; bottom: -10px; left: 0; display: inline-block; width: 100%; height: 10px; content: ""; } .carousel-indicators .active { background-color: #fff; } .carousel-caption { position: absolute; right: 15%; bottom: 20px; left: 15%; z-index: 10; padding-top: 20px; padding-bottom: 20px; color: #fff; text-align: center; } .align-baseline { vertical-align: baseline !important; } .align-top { vertical-align: top !important; } .align-middle { vertical-align: middle !important; } .align-bottom { vertical-align: bottom !important; } .align-text-bottom { vertical-align: text-bottom !important; } .align-text-top { vertical-align: text-top !important; } .bg-primary { background-color: #007bff !important; } a.bg-primary:hover, a.bg-primary:focus, button.bg-primary:hover, button.bg-primary:focus { background-color: #0062cc !important; } .bg-secondary { background-color: #6c757d !important; } a.bg-secondary:hover, a.bg-secondary:focus, button.bg-secondary:hover, button.bg-secondary:focus { background-color: #545b62 !important; } .bg-success { background-color: #28a745 !important; } a.bg-success:hover, a.bg-success:focus, button.bg-success:hover, button.bg-success:focus { background-color: #1e7e34 !important; } .bg-info { background-color: #17a2b8 !important; } a.bg-info:hover, a.bg-info:focus, button.bg-info:hover, button.bg-info:focus { background-color: #117a8b !important; } .bg-warning { background-color: #ffc107 !important; } a.bg-warning:hover, a.bg-warning:focus, button.bg-warning:hover, button.bg-warning:focus { background-color: #d39e00 !important; } .bg-danger { background-color: #dc3545 !important; } a.bg-danger:hover, a.bg-danger:focus, button.bg-danger:hover, button.bg-danger:focus { background-color: #bd2130 !important; } .bg-light { background-color: #f8f9fa !important; } a.bg-light:hover, a.bg-light:focus, button.bg-light:hover, button.bg-light:focus { background-color: #dae0e5 !important; } .bg-dark { background-color: #343a40 !important; } a.bg-dark:hover, a.bg-dark:focus, button.bg-dark:hover, button.bg-dark:focus { background-color: #1d2124 !important; } .bg-white { background-color: #fff !important; } .bg-transparent { background-color: transparent !important; } .border { border: 1px solid #dee2e6 !important; } .border-top { border-top: 1px solid #dee2e6 !important; } .border-right { border-right: 1px solid #dee2e6 !important; } .border-bottom { border-bottom: 1px solid #dee2e6 !important; } .border-left { border-left: 1px solid #dee2e6 !important; } .border-0 { border: 0 !important; } .border-top-0 { border-top: 0 !important; } .border-right-0 { border-right: 0 !important; } .border-bottom-0 { border-bottom: 0 !important; } .border-left-0 { border-left: 0 !important; } .border-primary { border-color: #007bff !important; } .border-secondary { border-color: #6c757d !important; } .border-success { border-color: #28a745 !important; } .border-info { border-color: #17a2b8 !important; } .border-warning { border-color: #ffc107 !important; } .border-danger { border-color: #dc3545 !important; } .border-light { border-color: #f8f9fa !important; } .border-dark { border-color: #343a40 !important; } .border-white { border-color: #fff !important; } .rounded { border-radius: 0.25rem !important; } .rounded-top { border-top-left-radius: 0.25rem !important; border-top-right-radius: 0.25rem !important; } .rounded-right { border-top-right-radius: 0.25rem !important; border-bottom-right-radius: 0.25rem !important; } .rounded-bottom { border-bottom-right-radius: 0.25rem !important; border-bottom-left-radius: 0.25rem !important; } .rounded-left { border-top-left-radius: 0.25rem !important; border-bottom-left-radius: 0.25rem !important; } .rounded-circle { border-radius: 50% !important; } .rounded-0 { border-radius: 0 !important; } .clearfix::after { display: block; clear: both; content: ""; } .d-none { display: none !important; } .d-inline { display: inline !important; } .d-inline-block { display: inline-block !important; } .d-block { display: block !important; } .d-table { display: table !important; } .d-table-row { display: table-row !important; } .d-table-cell { display: table-cell !important; } .d-flex { display: -ms-flexbox !important; display: flex !important; } .d-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } @media (min-width: 576px) { .d-sm-none { display: none !important; } .d-sm-inline { display: inline !important; } .d-sm-inline-block { display: inline-block !important; } .d-sm-block { display: block !important; } .d-sm-table { display: table !important; } .d-sm-table-row { display: table-row !important; } .d-sm-table-cell { display: table-cell !important; } .d-sm-flex { display: -ms-flexbox !important; display: flex !important; } .d-sm-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } @media (min-width: 768px) { .d-md-none { display: none !important; } .d-md-inline { display: inline !important; } .d-md-inline-block { display: inline-block !important; } .d-md-block { display: block !important; } .d-md-table { display: table !important; } .d-md-table-row { display: table-row !important; } .d-md-table-cell { display: table-cell !important; } .d-md-flex { display: -ms-flexbox !important; display: flex !important; } .d-md-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } @media (min-width: 992px) { .d-lg-none { display: none !important; } .d-lg-inline { display: inline !important; } .d-lg-inline-block { display: inline-block !important; } .d-lg-block { display: block !important; } .d-lg-table { display: table !important; } .d-lg-table-row { display: table-row !important; } .d-lg-table-cell { display: table-cell !important; } .d-lg-flex { display: -ms-flexbox !important; display: flex !important; } .d-lg-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } @media (min-width: 1200px) { .d-xl-none { display: none !important; } .d-xl-inline { display: inline !important; } .d-xl-inline-block { display: inline-block !important; } .d-xl-block { display: block !important; } .d-xl-table { display: table !important; } .d-xl-table-row { display: table-row !important; } .d-xl-table-cell { display: table-cell !important; } .d-xl-flex { display: -ms-flexbox !important; display: flex !important; } .d-xl-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } @media print { .d-print-none { display: none !important; } .d-print-inline { display: inline !important; } .d-print-inline-block { display: inline-block !important; } .d-print-block { display: block !important; } .d-print-table { display: table !important; } .d-print-table-row { display: table-row !important; } .d-print-table-cell { display: table-cell !important; } .d-print-flex { display: -ms-flexbox !important; display: flex !important; } .d-print-inline-flex { display: -ms-inline-flexbox !important; display: inline-flex !important; } } .embed-responsive { position: relative; display: block; width: 100%; padding: 0; overflow: hidden; } .embed-responsive::before { display: block; content: ""; } .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video { position: absolute; top: 0; bottom: 0; left: 0; width: 100%; height: 100%; border: 0; } .embed-responsive-21by9::before { padding-top: 42.857143%; } .embed-responsive-16by9::before { padding-top: 56.25%; } .embed-responsive-4by3::before { padding-top: 75%; } .embed-responsive-1by1::before { padding-top: 100%; } .flex-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } @media (min-width: 576px) { .flex-sm-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-sm-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-sm-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-sm-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-sm-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-sm-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-sm-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-sm-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-sm-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-sm-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-sm-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-sm-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-sm-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-sm-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-sm-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-sm-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-sm-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-sm-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-sm-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-sm-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-sm-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-sm-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-sm-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-sm-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-sm-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-sm-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-sm-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-sm-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-sm-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-sm-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-sm-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-sm-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-sm-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-sm-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } } @media (min-width: 768px) { .flex-md-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-md-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-md-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-md-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-md-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-md-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-md-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-md-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-md-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-md-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-md-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-md-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-md-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-md-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-md-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-md-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-md-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-md-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-md-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-md-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-md-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-md-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-md-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-md-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-md-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-md-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-md-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-md-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-md-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-md-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-md-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-md-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-md-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-md-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } } @media (min-width: 992px) { .flex-lg-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-lg-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-lg-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-lg-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-lg-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-lg-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-lg-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-lg-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-lg-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-lg-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-lg-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-lg-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-lg-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-lg-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-lg-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-lg-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-lg-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-lg-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-lg-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-lg-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-lg-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-lg-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-lg-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-lg-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-lg-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-lg-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-lg-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-lg-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-lg-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-lg-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-lg-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-lg-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-lg-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-lg-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } } @media (min-width: 1200px) { .flex-xl-row { -ms-flex-direction: row !important; flex-direction: row !important; } .flex-xl-column { -ms-flex-direction: column !important; flex-direction: column !important; } .flex-xl-row-reverse { -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; } .flex-xl-column-reverse { -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; } .flex-xl-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; } .flex-xl-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; } .flex-xl-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; } .flex-xl-fill { -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; } .flex-xl-grow-0 { -ms-flex-positive: 0 !important; flex-grow: 0 !important; } .flex-xl-grow-1 { -ms-flex-positive: 1 !important; flex-grow: 1 !important; } .flex-xl-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; } .flex-xl-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; } .justify-content-xl-start { -ms-flex-pack: start !important; justify-content: flex-start !important; } .justify-content-xl-end { -ms-flex-pack: end !important; justify-content: flex-end !important; } .justify-content-xl-center { -ms-flex-pack: center !important; justify-content: center !important; } .justify-content-xl-between { -ms-flex-pack: justify !important; justify-content: space-between !important; } .justify-content-xl-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; } .align-items-xl-start { -ms-flex-align: start !important; align-items: flex-start !important; } .align-items-xl-end { -ms-flex-align: end !important; align-items: flex-end !important; } .align-items-xl-center { -ms-flex-align: center !important; align-items: center !important; } .align-items-xl-baseline { -ms-flex-align: baseline !important; align-items: baseline !important; } .align-items-xl-stretch { -ms-flex-align: stretch !important; align-items: stretch !important; } .align-content-xl-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; } .align-content-xl-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; } .align-content-xl-center { -ms-flex-line-pack: center !important; align-content: center !important; } .align-content-xl-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; } .align-content-xl-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; } .align-content-xl-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; } .align-self-xl-auto { -ms-flex-item-align: auto !important; align-self: auto !important; } .align-self-xl-start { -ms-flex-item-align: start !important; align-self: flex-start !important; } .align-self-xl-end { -ms-flex-item-align: end !important; align-self: flex-end !important; } .align-self-xl-center { -ms-flex-item-align: center !important; align-self: center !important; } .align-self-xl-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; } .align-self-xl-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; } } .float-left { float: left !important; } .float-right { float: right !important; } .float-none { float: none !important; } @media (min-width: 576px) { .float-sm-left { float: left !important; } .float-sm-right { float: right !important; } .float-sm-none { float: none !important; } } @media (min-width: 768px) { .float-md-left { float: left !important; } .float-md-right { float: right !important; } .float-md-none { float: none !important; } } @media (min-width: 992px) { .float-lg-left { float: left !important; } .float-lg-right { float: right !important; } .float-lg-none { float: none !important; } } @media (min-width: 1200px) { .float-xl-left { float: left !important; } .float-xl-right { float: right !important; } .float-xl-none { float: none !important; } } .position-static { position: static !important; } .position-relative { position: relative !important; } .position-absolute { position: absolute !important; } .position-fixed { position: fixed !important; } .position-sticky { position: -webkit-sticky !important; position: sticky !important; } .fixed-top { position: fixed; top: 0; right: 0; left: 0; z-index: 1030; } .fixed-bottom { position: fixed; right: 0; bottom: 0; left: 0; z-index: 1030; } @supports ((position: -webkit-sticky) or (position: sticky)) { .sticky-top { position: -webkit-sticky; position: sticky; top: 0; z-index: 1020; } } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } .sr-only-focusable:active, .sr-only-focusable:focus { position: static; width: auto; height: auto; overflow: visible; clip: auto; white-space: normal; } .shadow-sm { box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; } .shadow { box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; } .shadow-lg { box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; } .shadow-none { box-shadow: none !important; } .w-25 { width: 25% !important; } .w-50 { width: 50% !important; } .w-75 { width: 75% !important; } .w-100 { width: 100% !important; } .w-auto { width: auto !important; } .h-25 { height: 25% !important; } .h-50 { height: 50% !important; } .h-75 { height: 75% !important; } .h-100 { height: 100% !important; } .h-auto { height: auto !important; } .mw-100 { max-width: 100% !important; } .mh-100 { max-height: 100% !important; } .m-0 { margin: 0 !important; } .mt-0, .my-0 { margin-top: 0 !important; } .mr-0, .mx-0 { margin-right: 0 !important; } .mb-0, .my-0 { margin-bottom: 0 !important; } .ml-0, .mx-0 { margin-left: 0 !important; } .m-1 { margin: 0.25rem !important; } .mt-1, .my-1 { margin-top: 0.25rem !important; } .mr-1, .mx-1 { margin-right: 0.25rem !important; } .mb-1, .my-1 { margin-bottom: 0.25rem !important; } .ml-1, .mx-1 { margin-left: 0.25rem !important; } .m-2 { margin: 0.5rem !important; } .mt-2, .my-2 { margin-top: 0.5rem !important; } .mr-2, .mx-2 { margin-right: 0.5rem !important; } .mb-2, .my-2 { margin-bottom: 0.5rem !important; } .ml-2, .mx-2 { margin-left: 0.5rem !important; } .m-3 { margin: 1rem !important; } .mt-3, .my-3 { margin-top: 1rem !important; } .mr-3, .mx-3 { margin-right: 1rem !important; } .mb-3, .my-3 { margin-bottom: 1rem !important; } .ml-3, .mx-3 { margin-left: 1rem !important; } .m-4 { margin: 1.5rem !important; } .mt-4, .my-4 { margin-top: 1.5rem !important; } .mr-4, .mx-4 { margin-right: 1.5rem !important; } .mb-4, .my-4 { margin-bottom: 1.5rem !important; } .ml-4, .mx-4 { margin-left: 1.5rem !important; } .m-5 { margin: 3rem !important; } .mt-5, .my-5 { margin-top: 3rem !important; } .mr-5, .mx-5 { margin-right: 3rem !important; } .mb-5, .my-5 { margin-bottom: 3rem !important; } .ml-5, .mx-5 { margin-left: 3rem !important; } .p-0 { padding: 0 !important; } .pt-0, .py-0 { padding-top: 0 !important; } .pr-0, .px-0 { padding-right: 0 !important; } .pb-0, .py-0 { padding-bottom: 0 !important; } .pl-0, .px-0 { padding-left: 0 !important; } .p-1 { padding: 0.25rem !important; } .pt-1, .py-1 { padding-top: 0.25rem !important; } .pr-1, .px-1 { padding-right: 0.25rem !important; } .pb-1, .py-1 { padding-bottom: 0.25rem !important; } .pl-1, .px-1 { padding-left: 0.25rem !important; } .p-2 { padding: 0.5rem !important; } .pt-2, .py-2 { padding-top: 0.5rem !important; } .pr-2, .px-2 { padding-right: 0.5rem !important; } .pb-2, .py-2 { padding-bottom: 0.5rem !important; } .pl-2, .px-2 { padding-left: 0.5rem !important; } .p-3 { padding: 1rem !important; } .pt-3, .py-3 { padding-top: 1rem !important; } .pr-3, .px-3 { padding-right: 1rem !important; } .pb-3, .py-3 { padding-bottom: 1rem !important; } .pl-3, .px-3 { padding-left: 1rem !important; } .p-4 { padding: 1.5rem !important; } .pt-4, .py-4 { padding-top: 1.5rem !important; } .pr-4, .px-4 { padding-right: 1.5rem !important; } .pb-4, .py-4 { padding-bottom: 1.5rem !important; } .pl-4, .px-4 { padding-left: 1.5rem !important; } .p-5 { padding: 3rem !important; } .pt-5, .py-5 { padding-top: 3rem !important; } .pr-5, .px-5 { padding-right: 3rem !important; } .pb-5, .py-5 { padding-bottom: 3rem !important; } .pl-5, .px-5 { padding-left: 3rem !important; } .m-auto { margin: auto !important; } .mt-auto, .my-auto { margin-top: auto !important; } .mr-auto, .mx-auto { margin-right: auto !important; } .mb-auto, .my-auto { margin-bottom: auto !important; } .ml-auto, .mx-auto { margin-left: auto !important; } @media (min-width: 576px) { .m-sm-0 { margin: 0 !important; } .mt-sm-0, .my-sm-0 { margin-top: 0 !important; } .mr-sm-0, .mx-sm-0 { margin-right: 0 !important; } .mb-sm-0, .my-sm-0 { margin-bottom: 0 !important; } .ml-sm-0, .mx-sm-0 { margin-left: 0 !important; } .m-sm-1 { margin: 0.25rem !important; } .mt-sm-1, .my-sm-1 { margin-top: 0.25rem !important; } .mr-sm-1, .mx-sm-1 { margin-right: 0.25rem !important; } .mb-sm-1, .my-sm-1 { margin-bottom: 0.25rem !important; } .ml-sm-1, .mx-sm-1 { margin-left: 0.25rem !important; } .m-sm-2 { margin: 0.5rem !important; } .mt-sm-2, .my-sm-2 { margin-top: 0.5rem !important; } .mr-sm-2, .mx-sm-2 { margin-right: 0.5rem !important; } .mb-sm-2, .my-sm-2 { margin-bottom: 0.5rem !important; } .ml-sm-2, .mx-sm-2 { margin-left: 0.5rem !important; } .m-sm-3 { margin: 1rem !important; } .mt-sm-3, .my-sm-3 { margin-top: 1rem !important; } .mr-sm-3, .mx-sm-3 { margin-right: 1rem !important; } .mb-sm-3, .my-sm-3 { margin-bottom: 1rem !important; } .ml-sm-3, .mx-sm-3 { margin-left: 1rem !important; } .m-sm-4 { margin: 1.5rem !important; } .mt-sm-4, .my-sm-4 { margin-top: 1.5rem !important; } .mr-sm-4, .mx-sm-4 { margin-right: 1.5rem !important; } .mb-sm-4, .my-sm-4 { margin-bottom: 1.5rem !important; } .ml-sm-4, .mx-sm-4 { margin-left: 1.5rem !important; } .m-sm-5 { margin: 3rem !important; } .mt-sm-5, .my-sm-5 { margin-top: 3rem !important; } .mr-sm-5, .mx-sm-5 { margin-right: 3rem !important; } .mb-sm-5, .my-sm-5 { margin-bottom: 3rem !important; } .ml-sm-5, .mx-sm-5 { margin-left: 3rem !important; } .p-sm-0 { padding: 0 !important; } .pt-sm-0, .py-sm-0 { padding-top: 0 !important; } .pr-sm-0, .px-sm-0 { padding-right: 0 !important; } .pb-sm-0, .py-sm-0 { padding-bottom: 0 !important; } .pl-sm-0, .px-sm-0 { padding-left: 0 !important; } .p-sm-1 { padding: 0.25rem !important; } .pt-sm-1, .py-sm-1 { padding-top: 0.25rem !important; } .pr-sm-1, .px-sm-1 { padding-right: 0.25rem !important; } .pb-sm-1, .py-sm-1 { padding-bottom: 0.25rem !important; } .pl-sm-1, .px-sm-1 { padding-left: 0.25rem !important; } .p-sm-2 { padding: 0.5rem !important; } .pt-sm-2, .py-sm-2 { padding-top: 0.5rem !important; } .pr-sm-2, .px-sm-2 { padding-right: 0.5rem !important; } .pb-sm-2, .py-sm-2 { padding-bottom: 0.5rem !important; } .pl-sm-2, .px-sm-2 { padding-left: 0.5rem !important; } .p-sm-3 { padding: 1rem !important; } .pt-sm-3, .py-sm-3 { padding-top: 1rem !important; } .pr-sm-3, .px-sm-3 { padding-right: 1rem !important; } .pb-sm-3, .py-sm-3 { padding-bottom: 1rem !important; } .pl-sm-3, .px-sm-3 { padding-left: 1rem !important; } .p-sm-4 { padding: 1.5rem !important; } .pt-sm-4, .py-sm-4 { padding-top: 1.5rem !important; } .pr-sm-4, .px-sm-4 { padding-right: 1.5rem !important; } .pb-sm-4, .py-sm-4 { padding-bottom: 1.5rem !important; } .pl-sm-4, .px-sm-4 { padding-left: 1.5rem !important; } .p-sm-5 { padding: 3rem !important; } .pt-sm-5, .py-sm-5 { padding-top: 3rem !important; } .pr-sm-5, .px-sm-5 { padding-right: 3rem !important; } .pb-sm-5, .py-sm-5 { padding-bottom: 3rem !important; } .pl-sm-5, .px-sm-5 { padding-left: 3rem !important; } .m-sm-auto { margin: auto !important; } .mt-sm-auto, .my-sm-auto { margin-top: auto !important; } .mr-sm-auto, .mx-sm-auto { margin-right: auto !important; } .mb-sm-auto, .my-sm-auto { margin-bottom: auto !important; } .ml-sm-auto, .mx-sm-auto { margin-left: auto !important; } } @media (min-width: 768px) { .m-md-0 { margin: 0 !important; } .mt-md-0, .my-md-0 { margin-top: 0 !important; } .mr-md-0, .mx-md-0 { margin-right: 0 !important; } .mb-md-0, .my-md-0 { margin-bottom: 0 !important; } .ml-md-0, .mx-md-0 { margin-left: 0 !important; } .m-md-1 { margin: 0.25rem !important; } .mt-md-1, .my-md-1 { margin-top: 0.25rem !important; } .mr-md-1, .mx-md-1 { margin-right: 0.25rem !important; } .mb-md-1, .my-md-1 { margin-bottom: 0.25rem !important; } .ml-md-1, .mx-md-1 { margin-left: 0.25rem !important; } .m-md-2 { margin: 0.5rem !important; } .mt-md-2, .my-md-2 { margin-top: 0.5rem !important; } .mr-md-2, .mx-md-2 { margin-right: 0.5rem !important; } .mb-md-2, .my-md-2 { margin-bottom: 0.5rem !important; } .ml-md-2, .mx-md-2 { margin-left: 0.5rem !important; } .m-md-3 { margin: 1rem !important; } .mt-md-3, .my-md-3 { margin-top: 1rem !important; } .mr-md-3, .mx-md-3 { margin-right: 1rem !important; } .mb-md-3, .my-md-3 { margin-bottom: 1rem !important; } .ml-md-3, .mx-md-3 { margin-left: 1rem !important; } .m-md-4 { margin: 1.5rem !important; } .mt-md-4, .my-md-4 { margin-top: 1.5rem !important; } .mr-md-4, .mx-md-4 { margin-right: 1.5rem !important; } .mb-md-4, .my-md-4 { margin-bottom: 1.5rem !important; } .ml-md-4, .mx-md-4 { margin-left: 1.5rem !important; } .m-md-5 { margin: 3rem !important; } .mt-md-5, .my-md-5 { margin-top: 3rem !important; } .mr-md-5, .mx-md-5 { margin-right: 3rem !important; } .mb-md-5, .my-md-5 { margin-bottom: 3rem !important; } .ml-md-5, .mx-md-5 { margin-left: 3rem !important; } .p-md-0 { padding: 0 !important; } .pt-md-0, .py-md-0 { padding-top: 0 !important; } .pr-md-0, .px-md-0 { padding-right: 0 !important; } .pb-md-0, .py-md-0 { padding-bottom: 0 !important; } .pl-md-0, .px-md-0 { padding-left: 0 !important; } .p-md-1 { padding: 0.25rem !important; } .pt-md-1, .py-md-1 { padding-top: 0.25rem !important; } .pr-md-1, .px-md-1 { padding-right: 0.25rem !important; } .pb-md-1, .py-md-1 { padding-bottom: 0.25rem !important; } .pl-md-1, .px-md-1 { padding-left: 0.25rem !important; } .p-md-2 { padding: 0.5rem !important; } .pt-md-2, .py-md-2 { padding-top: 0.5rem !important; } .pr-md-2, .px-md-2 { padding-right: 0.5rem !important; } .pb-md-2, .py-md-2 { padding-bottom: 0.5rem !important; } .pl-md-2, .px-md-2 { padding-left: 0.5rem !important; } .p-md-3 { padding: 1rem !important; } .pt-md-3, .py-md-3 { padding-top: 1rem !important; } .pr-md-3, .px-md-3 { padding-right: 1rem !important; } .pb-md-3, .py-md-3 { padding-bottom: 1rem !important; } .pl-md-3, .px-md-3 { padding-left: 1rem !important; } .p-md-4 { padding: 1.5rem !important; } .pt-md-4, .py-md-4 { padding-top: 1.5rem !important; } .pr-md-4, .px-md-4 { padding-right: 1.5rem !important; } .pb-md-4, .py-md-4 { padding-bottom: 1.5rem !important; } .pl-md-4, .px-md-4 { padding-left: 1.5rem !important; } .p-md-5 { padding: 3rem !important; } .pt-md-5, .py-md-5 { padding-top: 3rem !important; } .pr-md-5, .px-md-5 { padding-right: 3rem !important; } .pb-md-5, .py-md-5 { padding-bottom: 3rem !important; } .pl-md-5, .px-md-5 { padding-left: 3rem !important; } .m-md-auto { margin: auto !important; } .mt-md-auto, .my-md-auto { margin-top: auto !important; } .mr-md-auto, .mx-md-auto { margin-right: auto !important; } .mb-md-auto, .my-md-auto { margin-bottom: auto !important; } .ml-md-auto, .mx-md-auto { margin-left: auto !important; } } @media (min-width: 992px) { .m-lg-0 { margin: 0 !important; } .mt-lg-0, .my-lg-0 { margin-top: 0 !important; } .mr-lg-0, .mx-lg-0 { margin-right: 0 !important; } .mb-lg-0, .my-lg-0 { margin-bottom: 0 !important; } .ml-lg-0, .mx-lg-0 { margin-left: 0 !important; } .m-lg-1 { margin: 0.25rem !important; } .mt-lg-1, .my-lg-1 { margin-top: 0.25rem !important; } .mr-lg-1, .mx-lg-1 { margin-right: 0.25rem !important; } .mb-lg-1, .my-lg-1 { margin-bottom: 0.25rem !important; } .ml-lg-1, .mx-lg-1 { margin-left: 0.25rem !important; } .m-lg-2 { margin: 0.5rem !important; } .mt-lg-2, .my-lg-2 { margin-top: 0.5rem !important; } .mr-lg-2, .mx-lg-2 { margin-right: 0.5rem !important; } .mb-lg-2, .my-lg-2 { margin-bottom: 0.5rem !important; } .ml-lg-2, .mx-lg-2 { margin-left: 0.5rem !important; } .m-lg-3 { margin: 1rem !important; } .mt-lg-3, .my-lg-3 { margin-top: 1rem !important; } .mr-lg-3, .mx-lg-3 { margin-right: 1rem !important; } .mb-lg-3, .my-lg-3 { margin-bottom: 1rem !important; } .ml-lg-3, .mx-lg-3 { margin-left: 1rem !important; } .m-lg-4 { margin: 1.5rem !important; } .mt-lg-4, .my-lg-4 { margin-top: 1.5rem !important; } .mr-lg-4, .mx-lg-4 { margin-right: 1.5rem !important; } .mb-lg-4, .my-lg-4 { margin-bottom: 1.5rem !important; } .ml-lg-4, .mx-lg-4 { margin-left: 1.5rem !important; } .m-lg-5 { margin: 3rem !important; } .mt-lg-5, .my-lg-5 { margin-top: 3rem !important; } .mr-lg-5, .mx-lg-5 { margin-right: 3rem !important; } .mb-lg-5, .my-lg-5 { margin-bottom: 3rem !important; } .ml-lg-5, .mx-lg-5 { margin-left: 3rem !important; } .p-lg-0 { padding: 0 !important; } .pt-lg-0, .py-lg-0 { padding-top: 0 !important; } .pr-lg-0, .px-lg-0 { padding-right: 0 !important; } .pb-lg-0, .py-lg-0 { padding-bottom: 0 !important; } .pl-lg-0, .px-lg-0 { padding-left: 0 !important; } .p-lg-1 { padding: 0.25rem !important; } .pt-lg-1, .py-lg-1 { padding-top: 0.25rem !important; } .pr-lg-1, .px-lg-1 { padding-right: 0.25rem !important; } .pb-lg-1, .py-lg-1 { padding-bottom: 0.25rem !important; } .pl-lg-1, .px-lg-1 { padding-left: 0.25rem !important; } .p-lg-2 { padding: 0.5rem !important; } .pt-lg-2, .py-lg-2 { padding-top: 0.5rem !important; } .pr-lg-2, .px-lg-2 { padding-right: 0.5rem !important; } .pb-lg-2, .py-lg-2 { padding-bottom: 0.5rem !important; } .pl-lg-2, .px-lg-2 { padding-left: 0.5rem !important; } .p-lg-3 { padding: 1rem !important; } .pt-lg-3, .py-lg-3 { padding-top: 1rem !important; } .pr-lg-3, .px-lg-3 { padding-right: 1rem !important; } .pb-lg-3, .py-lg-3 { padding-bottom: 1rem !important; } .pl-lg-3, .px-lg-3 { padding-left: 1rem !important; } .p-lg-4 { padding: 1.5rem !important; } .pt-lg-4, .py-lg-4 { padding-top: 1.5rem !important; } .pr-lg-4, .px-lg-4 { padding-right: 1.5rem !important; } .pb-lg-4, .py-lg-4 { padding-bottom: 1.5rem !important; } .pl-lg-4, .px-lg-4 { padding-left: 1.5rem !important; } .p-lg-5 { padding: 3rem !important; } .pt-lg-5, .py-lg-5 { padding-top: 3rem !important; } .pr-lg-5, .px-lg-5 { padding-right: 3rem !important; } .pb-lg-5, .py-lg-5 { padding-bottom: 3rem !important; } .pl-lg-5, .px-lg-5 { padding-left: 3rem !important; } .m-lg-auto { margin: auto !important; } .mt-lg-auto, .my-lg-auto { margin-top: auto !important; } .mr-lg-auto, .mx-lg-auto { margin-right: auto !important; } .mb-lg-auto, .my-lg-auto { margin-bottom: auto !important; } .ml-lg-auto, .mx-lg-auto { margin-left: auto !important; } } @media (min-width: 1200px) { .m-xl-0 { margin: 0 !important; } .mt-xl-0, .my-xl-0 { margin-top: 0 !important; } .mr-xl-0, .mx-xl-0 { margin-right: 0 !important; } .mb-xl-0, .my-xl-0 { margin-bottom: 0 !important; } .ml-xl-0, .mx-xl-0 { margin-left: 0 !important; } .m-xl-1 { margin: 0.25rem !important; } .mt-xl-1, .my-xl-1 { margin-top: 0.25rem !important; } .mr-xl-1, .mx-xl-1 { margin-right: 0.25rem !important; } .mb-xl-1, .my-xl-1 { margin-bottom: 0.25rem !important; } .ml-xl-1, .mx-xl-1 { margin-left: 0.25rem !important; } .m-xl-2 { margin: 0.5rem !important; } .mt-xl-2, .my-xl-2 { margin-top: 0.5rem !important; } .mr-xl-2, .mx-xl-2 { margin-right: 0.5rem !important; } .mb-xl-2, .my-xl-2 { margin-bottom: 0.5rem !important; } .ml-xl-2, .mx-xl-2 { margin-left: 0.5rem !important; } .m-xl-3 { margin: 1rem !important; } .mt-xl-3, .my-xl-3 { margin-top: 1rem !important; } .mr-xl-3, .mx-xl-3 { margin-right: 1rem !important; } .mb-xl-3, .my-xl-3 { margin-bottom: 1rem !important; } .ml-xl-3, .mx-xl-3 { margin-left: 1rem !important; } .m-xl-4 { margin: 1.5rem !important; } .mt-xl-4, .my-xl-4 { margin-top: 1.5rem !important; } .mr-xl-4, .mx-xl-4 { margin-right: 1.5rem !important; } .mb-xl-4, .my-xl-4 { margin-bottom: 1.5rem !important; } .ml-xl-4, .mx-xl-4 { margin-left: 1.5rem !important; } .m-xl-5 { margin: 3rem !important; } .mt-xl-5, .my-xl-5 { margin-top: 3rem !important; } .mr-xl-5, .mx-xl-5 { margin-right: 3rem !important; } .mb-xl-5, .my-xl-5 { margin-bottom: 3rem !important; } .ml-xl-5, .mx-xl-5 { margin-left: 3rem !important; } .p-xl-0 { padding: 0 !important; } .pt-xl-0, .py-xl-0 { padding-top: 0 !important; } .pr-xl-0, .px-xl-0 { padding-right: 0 !important; } .pb-xl-0, .py-xl-0 { padding-bottom: 0 !important; } .pl-xl-0, .px-xl-0 { padding-left: 0 !important; } .p-xl-1 { padding: 0.25rem !important; } .pt-xl-1, .py-xl-1 { padding-top: 0.25rem !important; } .pr-xl-1, .px-xl-1 { padding-right: 0.25rem !important; } .pb-xl-1, .py-xl-1 { padding-bottom: 0.25rem !important; } .pl-xl-1, .px-xl-1 { padding-left: 0.25rem !important; } .p-xl-2 { padding: 0.5rem !important; } .pt-xl-2, .py-xl-2 { padding-top: 0.5rem !important; } .pr-xl-2, .px-xl-2 { padding-right: 0.5rem !important; } .pb-xl-2, .py-xl-2 { padding-bottom: 0.5rem !important; } .pl-xl-2, .px-xl-2 { padding-left: 0.5rem !important; } .p-xl-3 { padding: 1rem !important; } .pt-xl-3, .py-xl-3 { padding-top: 1rem !important; } .pr-xl-3, .px-xl-3 { padding-right: 1rem !important; } .pb-xl-3, .py-xl-3 { padding-bottom: 1rem !important; } .pl-xl-3, .px-xl-3 { padding-left: 1rem !important; } .p-xl-4 { padding: 1.5rem !important; } .pt-xl-4, .py-xl-4 { padding-top: 1.5rem !important; } .pr-xl-4, .px-xl-4 { padding-right: 1.5rem !important; } .pb-xl-4, .py-xl-4 { padding-bottom: 1.5rem !important; } .pl-xl-4, .px-xl-4 { padding-left: 1.5rem !important; } .p-xl-5 { padding: 3rem !important; } .pt-xl-5, .py-xl-5 { padding-top: 3rem !important; } .pr-xl-5, .px-xl-5 { padding-right: 3rem !important; } .pb-xl-5, .py-xl-5 { padding-bottom: 3rem !important; } .pl-xl-5, .px-xl-5 { padding-left: 3rem !important; } .m-xl-auto { margin: auto !important; } .mt-xl-auto, .my-xl-auto { margin-top: auto !important; } .mr-xl-auto, .mx-xl-auto { margin-right: auto !important; } .mb-xl-auto, .my-xl-auto { margin-bottom: auto !important; } .ml-xl-auto, .mx-xl-auto { margin-left: auto !important; } } .text-monospace { font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } .text-justify { text-align: justify !important; } .text-nowrap { white-space: nowrap !important; } .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .text-left { text-align: left !important; } .text-right { text-align: right !important; } .text-center { text-align: center !important; } @media (min-width: 576px) { .text-sm-left { text-align: left !important; } .text-sm-right { text-align: right !important; } .text-sm-center { text-align: center !important; } } @media (min-width: 768px) { .text-md-left { text-align: left !important; } .text-md-right { text-align: right !important; } .text-md-center { text-align: center !important; } } @media (min-width: 992px) { .text-lg-left { text-align: left !important; } .text-lg-right { text-align: right !important; } .text-lg-center { text-align: center !important; } } @media (min-width: 1200px) { .text-xl-left { text-align: left !important; } .text-xl-right { text-align: right !important; } .text-xl-center { text-align: center !important; } } .text-lowercase { text-transform: lowercase !important; } .text-uppercase { text-transform: uppercase !important; } .text-capitalize { text-transform: capitalize !important; } .font-weight-light { font-weight: 300 !important; } .font-weight-normal { font-weight: 400 !important; } .font-weight-bold { font-weight: 700 !important; } .font-italic { font-style: italic !important; } .text-white { color: #fff !important; } .text-primary { color: #007bff !important; } a.text-primary:hover, a.text-primary:focus { color: #0062cc !important; } .text-secondary { color: #6c757d !important; } a.text-secondary:hover, a.text-secondary:focus { color: #545b62 !important; } .text-success { color: #28a745 !important; } a.text-success:hover, a.text-success:focus { color: #1e7e34 !important; } .text-info { color: #17a2b8 !important; } a.text-info:hover, a.text-info:focus { color: #117a8b !important; } .text-warning { color: #ffc107 !important; } a.text-warning:hover, a.text-warning:focus { color: #d39e00 !important; } .text-danger { color: #dc3545 !important; } a.text-danger:hover, a.text-danger:focus { color: #bd2130 !important; } .text-light { color: #f8f9fa !important; } a.text-light:hover, a.text-light:focus { color: #dae0e5 !important; } .text-dark { color: #343a40 !important; } a.text-dark:hover, a.text-dark:focus { color: #1d2124 !important; } .text-body { color: #212529 !important; } .text-muted { color: #6c757d !important; } .text-black-50 { color: rgba(0, 0, 0, 0.5) !important; } .text-white-50 { color: rgba(255, 255, 255, 0.5) !important; } .text-hide { font: 0/0 a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } .visible { visibility: visible !important; } .invisible { visibility: hidden !important; } @media print { *, *::before, *::after { text-shadow: none !important; box-shadow: none !important; } a:not(.btn) { text-decoration: underline; } abbr[title]::after { content: " (" attr(title) ")"; } pre { white-space: pre-wrap !important; } pre, blockquote { border: 1px solid #adb5bd; page-break-inside: avoid; } thead { display: table-header-group; } tr, img { page-break-inside: avoid; } p, h2, h3 { orphans: 3; widows: 3; } h2, h3 { page-break-after: avoid; } @page { size: a3; } body { min-width: 992px !important; } .container { min-width: 992px !important; } .navbar { display: none; } .badge { border: 1px solid #000; } .table { border-collapse: collapse !important; } .table td, .table th { background-color: #fff !important; } .table-bordered th, .table-bordered td { border: 1px solid #dee2e6 !important; } .table-dark { color: inherit; } .table-dark th, .table-dark td, .table-dark thead th, .table-dark tbody + tbody { border-color: #dee2e6; } .table .thead-dark th { color: inherit; border-color: #dee2e6; } } /*# sourceMappingURL=bootstrap.css.map */ ================================================ FILE: src/main/resources/static/assets/css/prism.css ================================================ /* PrismJS 1.19.0 https://prismjs.com/download.html#themes=prism-okaidia&languages=bash+yaml&plugins=toolbar+copy-to-clipboard */ /** * okaidia theme for JavaScript, CSS and HTML * Loosely based on Monokai textmate theme by http://www.monokai.nl/ * @author ocodia */ code[class*="language-"], pre[class*="language-"] { color: #f8f8f2; background: none; text-shadow: 0 1px rgba(0, 0, 0, 0.3); font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; font-size: 1em; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } /* Code blocks */ pre[class*="language-"] { padding: 1em; margin: .5em 0; overflow: auto; border-radius: 0.3em; } :not(pre) > code[class*="language-"], pre[class*="language-"] { background: #272822; } /* Inline code */ :not(pre) > code[class*="language-"] { padding: .1em; border-radius: .3em; white-space: normal; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: slategray; } .token.punctuation { color: #f8f8f2; } .token.namespace { opacity: .7; } .token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #f92672; } .token.boolean, .token.number { color: #ae81ff; } .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #a6e22e; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable { color: #f8f8f2; } .token.atrule, .token.attr-value, .token.function, .token.class-name { color: #e6db74; } .token.keyword { color: #66d9ef; } .token.regex, .token.important { color: #fd971f; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } div.code-toolbar { position: relative; } div.code-toolbar > .toolbar { position: absolute; top: .3em; right: .2em; transition: opacity 0.3s ease-in-out; opacity: 0; } div.code-toolbar:hover > .toolbar { opacity: 1; } /* Separate line b/c rules are thrown out if selector is invalid. IE11 and old Edge versions don't support :focus-within. */ div.code-toolbar:focus-within > .toolbar { opacity: 1; } div.code-toolbar > .toolbar .toolbar-item { display: inline-block; } div.code-toolbar > .toolbar a { cursor: pointer; } div.code-toolbar > .toolbar button { background: none; border: 0; color: inherit; font: inherit; line-height: normal; overflow: visible; padding: 0; -webkit-user-select: none; /* for button */ -moz-user-select: none; -ms-user-select: none; } div.code-toolbar > .toolbar a, div.code-toolbar > .toolbar button, div.code-toolbar > .toolbar span { color: #bbb; font-size: .8em; padding: 0 .5em; background: #f5f2f0; background: rgba(224, 224, 224, 0.2); box-shadow: 0 2px 0 0 rgba(0,0,0,0.2); border-radius: .5em; } div.code-toolbar > .toolbar a:hover, div.code-toolbar > .toolbar a:focus, div.code-toolbar > .toolbar button:hover, div.code-toolbar > .toolbar button:focus, div.code-toolbar > .toolbar span:hover, div.code-toolbar > .toolbar span:focus { color: inherit; text-decoration: none; } ================================================ FILE: src/main/resources/static/assets/js/admin.js ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ var Admin = (function($) { 'use strict'; // Repositories var $SubmitNewRepository; var $NewRepositoryName; var $RepositoryPendingDeletion; var $DeleteRepository; // Image Edit var $ImageExternalUrls; var $AddNewExternalUrl; var $TrackNewBranch; var $NewTrackedBranch; var $ImageKey; // Image Template var $ImageTemplateTabContent; var $ImageTemplatePorts; var $AddNewPort; var $ImageTemplateVolumes; var $AddNewVolume; var $ImageTemplateEnv; var $AddNewEnv; var $ImageTemplateDevices; var $AddNewDevice; // Users var $UserNamePendingDeletion; var $UserPendingDeletion; var $UserPendingPasswordChange; var $UserNamePendingPasswordChange; var reload = function() { window.location.reload(); }; var cleanEmpty = function(val) { return (typeof val === 'undefined' || $.trim(val).length === 0) ? null : val; }; var runSchedule = function(trigger) { Ajax.put('/internalapi/schedule', { 'scheduleKey': trigger.data('schedule-key') }, function() { Notifications.makeNotification('Schedule run submitted successfully.', 'success'); }, trigger); }; var deleteRepository = function(trigger) { Ajax.del('/internalapi/repository?repositoryKey=' + trigger.data('repository-key'), reload, trigger); }; var syncRepository = function(trigger) { Ajax.put('/internalapi/repository/sync', { 'repositoryKey': trigger.data('repository-key') }, function() { Notifications.makeNotification('Sync request submitted.', 'success'); }, trigger); }; var addRepository = function(repositoryName) { var trimmedName = $.trim(repositoryName); if (trimmedName.length > 0) { Notifications.makeNotification('Verifying repository "' + trimmedName + '" and fetching images. Please wait...', 'info', '10000'); Ajax.postJson('/internalapi/repository', { 'repositoryName': trimmedName }, reload, $SubmitNewRepository, function() { $('#NewRepositoryName').val(''); }); } }; var updateRepositorySpec = function($row) { var data = { 'repositoryKey': $row.data('repository-key'), 'syncEnabled': $row.find('.editable-repository-enabled').find('input[type="checkbox"]').is(':checked'), 'versionMask': cleanEmpty($row.find('.editable-repository-version-mask').find('.switchable.field input[type="text"]').val()) }; Ajax.putJson('/internalapi/repository', data, function(data) { Notifications.makeNotification(data.name + ' updated', 'success'); }); }; var syncImage = function(trigger) { Ajax.put('/internalapi/image/sync', { 'imageKey': trigger.data('image-key') }, function() { Notifications.makeNotification('Sync request submitted.', 'success'); }, trigger); }; var updateImageSpec = function($row) { var data = { 'imageKey': $row.data('image-key'), 'syncEnabled': $row.find('.editable-image-sync-enabled').find('input[type="checkbox"]').is(':checked'), 'versionMask': cleanEmpty($row.find('.editable-image-version-mask').find('.switchable.field input[type="text"]').val()), 'stable': $row.find('.editable-image-stable').find('input[type="checkbox"]').is(':checked'), 'hidden': $row.find('.editable-image-hidden').find('input[type="checkbox"]').is(':checked'), 'deprecated': $row.find('.editable-image-deprecated').find('input[type="checkbox"]').is(':checked') }; Ajax.putJson('/internalapi/image', data, function() {}); }; var trackNewBranch = function(branchName, imageKey) { Ajax.put('/internalapi/image/track', { 'imageKey': imageKey, 'branchName': branchName }, reload, $TrackNewBranch); }; var removeTrackedBranch = function(branchName, imageKey) { Ajax.del('/internalapi/image/track?imageKey=' + imageKey +'&branchName=' + branchName, reload); }; var makeInput = function(name, type='text', required=false) { return '' }; var makeSelect = function(name, values=[]) { var options = ''; values.forEach(function(value) { options += ''; }); return ( '
' ); }; var makeButton = function(value, classes, colour) { return ''; }; var addPortRow = function() { $ImageTemplatePorts.find('tbody').append($( '' + '' + makeInput('imageTemplatePort', 'number', true) + '' + '' + makeSelect('imageTemplatePortProtocol', ['tcp', 'udp']) + '' + '' + makeInput('imageTemplatePortDescription') + '' + '' + '
' + makeButton('', 'remove-image-template-item', 'danger') + '
' + '' + '' )); }; var addExternalUrlRow = function() { $ImageExternalUrls.find('tbody').append($( '' + '' + makeSelect('imageExternalUrlType', externalUrlTypes) + '' + '' + '' + makeInput('imageExternalUrlName', 'text', true) + '' + '' + makeInput('imageExternalUrlPath', 'text', true) + '' + '' + '
' + makeButton('', 'remove-image-external-url', 'danger') + '
' + '' + '' )); }; var addVolumeRow = function() { $ImageTemplateVolumes.find('tbody').append($( '' + '' + makeInput('imageTemplateVolume', 'text', true) + '' + '' + makeSelect('imageTemplateVolumeReadonly', ['read-write', 'readonly']) + '' + '' + makeInput('imageTemplateVolumeDescription') + '' + '' + '
' + makeButton('', 'remove-image-template-item', 'danger') + '
' + '' + '' )); }; var addEnvRow = function() { $ImageTemplateEnv.find('tbody').append($( '' + '' + makeInput('imageTemplateEnv', 'text', true) + '' + '' + makeInput('imageTemplateEnvExample') + '' + '' + makeInput('imageTemplateEnvDescription') + '' + '' + '
' + makeButton('', 'remove-image-template-item', 'danger') + '
' + '' + '' )); }; var addDeviceRow = function() { $ImageTemplateDevices.find('tbody').append($( '' + '' + makeInput('imageTemplateDevice', 'text', true) + '' + '' + makeInput('imageTemplateDeviceDescription') + '' + '' + '
' + makeButton('', 'remove-image-template-item', 'danger') + '
' + '' + '' )); }; var init = function() { $SubmitNewRepository = $('#SubmitNewRepository'); $NewRepositoryName = $('#NewRepositoryName'); $TrackNewBranch = $('#TrackNewBranch'); $NewTrackedBranch = $('#NewTrackedBranch'); $RepositoryPendingDeletion = $('#RepositoryPendingDeletion'); $DeleteRepository = $('#DeleteRepository'); $ImageKey = $('#ImageKey'); $ImageTemplateTabContent = $('#ImageTemplateTabContent'); $ImageTemplatePorts = $('#ImageTemplatePorts'); $AddNewPort = $('#AddNewPort'); $ImageTemplateVolumes = $('#ImageTemplateVolumes'); $AddNewVolume = $('#AddNewVolume'); $ImageTemplateEnv = $('#ImageTemplateEnv'); $AddNewEnv = $('#AddNewEnv'); $ImageTemplateDevices = $('#ImageTemplateDevices'); $AddNewDevice = $('#AddNewDevice'); $ImageExternalUrls = $('#ImageExternalUrls'); $AddNewExternalUrl = $('#AddNewExternalUrl'); $UserPendingDeletion = $('#UserPendingDeletion'); $UserNamePendingDeletion = $('#UserNamePendingDeletion'); $UserNamePendingPasswordChange = $('#UserNamePendingPasswordChange'); $UserPendingPasswordChange = $('#UserPendingPasswordChange'); $SubmitNewRepository.on('click', function() { addRepository($NewRepositoryName.val()); }); $('.force-schedule-run').on('click', function() { runSchedule($(this)); }); $('.sync-repository').on('click', function() { syncRepository($(this)); }); $('.update-repository-trigger').on('click', function() { updateRepositorySpec($(this).parents('.repository-row')); }); $DeleteRepository.on('click', function() { deleteRepository($(this)); }); $('.delete-repository').on('click', function() { var $trigger = $(this); $RepositoryPendingDeletion.text($trigger.data('repository-name')); $DeleteRepository.data('repository-key', $trigger.data('repository-key')) }); $('.delete-user').on('click', function() { var $triggerParent = $(this).parents('tr'); $UserPendingDeletion.val($triggerParent.data('user-key')); $UserNamePendingDeletion.text($triggerParent.data('user-name')); }); $('.edit-password').on('click', function() { var $triggerParent = $(this).parents('tr'); $UserPendingPasswordChange.val($triggerParent.data('user-key')); $UserNamePendingPasswordChange.text($triggerParent.data('user-name')); }); $('.update-image-trigger').on('click', function() { updateImageSpec($(this).parents('.image-row')); }); $('.sync-image').on('click', function() { syncImage($(this)); }); $TrackNewBranch.on('click', function() { var branchName = $.trim($NewTrackedBranch.val()); if (branchName.length > 0) { trackNewBranch(branchName, $ImageKey.val()); } }); $('.remove-tag-branch').on('click', function() { var branchName = $(this).parents('.tracked-branch').data('branch-name'); removeTrackedBranch(branchName, $ImageKey.val()); }); $AddNewExternalUrl.on('click', addExternalUrlRow); $ImageExternalUrls.on('click', '.remove-image-external-url', function() { $(this).parents('tr').remove(); }); $AddNewPort.on('click', addPortRow); $AddNewVolume.on('click', addVolumeRow); $AddNewEnv.on('click', addEnvRow); $AddNewDevice.on('click', addDeviceRow); $ImageTemplateTabContent.on('click', '.remove-image-template-item', function() { $(this).parents('tr').remove(); }); }; return { init: init } }(jQuery)); ================================================ FILE: src/main/resources/static/assets/js/app.js ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ var Notifications = (function($) { var makeNotification = function(message, level='info', delay) { var notification = $( '
' + '' + message + '
' ); $('#Notifications').append(notification); notification.delay((typeof delay === 'undefined' ? 3000 : delay)).fadeOut(2000, function() { $(this).remove(); }); }; return { makeNotification: makeNotification } }(jQuery)); var Ajax = (function($) { var toggleLoadingState = function(button) { if (typeof button !== 'undefined' && null !== button) { button.prop('disabled', !button.prop('disabled')).toggleClass('is-loading'); } }; var handleError = function(jqXHR, onError) { $('.modal').removeClass('is-active'); if (jqXHR.status === 403) { Notifications.makeNotification('Permission denied', 'danger'); } else { Notifications.makeNotification(jqXHR.responseText, 'danger'); } if (onError) { onError(); } }; var call = function(param, onDone, triggerButton, onError) { toggleLoadingState(triggerButton); return $.ajax(param) .done(function(data) { if (onDone) onDone(data); }) .fail(function(jqXHR) { handleError(jqXHR, onError); }) .always(function() { toggleLoadingState(triggerButton); }); }; var get = function(url, params, onDone, triggerButton, onError) { call({ method: 'get', url: url, data: params}, onDone, triggerButton, onError); }; var put = function(url, params, onDone, triggerButton, onError) { call({ method: 'put', url: url, data: params}, onDone, triggerButton, onError); }; var putJson = function(url, params, onDone, triggerButton, onError) { call({ method: 'put', url: url, contentType: 'application/json', dataType: 'json', data: JSON.stringify(params) }, onDone, triggerButton, onError); }; var post = function(url, params, onDone, triggerButton, onError) { call({ method: 'post', url: url, data: params}, onDone, triggerButton, onError); }; var postJson = function(url, params, onDone, triggerButton, onError) { call({ method: 'post', url: url, contentType: 'application/json', dataType: 'json', data: JSON.stringify(params) }, onDone, triggerButton, onError); }; var del = function(url, onDone, triggerButton, onError) { call({ method: 'delete', url: url }, onDone, triggerButton, onError); }; return { call: call, get: get, put: put, putJson: putJson, post: post, postJson: postJson, del: del }; }(jQuery)); var FormValidation = (function($) { var setValid = function(element) { element.classList.remove('is-danger'); }; var setInvalid = function(element) { element.classList.add('is-danger'); }; var comparePasswords = function(password, verifyPassword) { if (password.value !== verifyPassword.value) { verifyPassword.setCustomValidity('Mismatch'); setInvalid(verifyPassword); } else { verifyPassword.setCustomValidity(''); setValid(verifyPassword); } }; var initPasswordVerification = function() { var verifyPassword = document.getElementById('password-confirm'); if (typeof verifyPassword !== 'undefined' && null !== verifyPassword) { var password = document.getElementById('password'); password.addEventListener('input', function(event) { comparePasswords(password, verifyPassword); }, false); verifyPassword.addEventListener('input', function(event) { comparePasswords(password, verifyPassword); }, false); } }; var initFormValidation = function() { var forms = document.getElementsByClassName('needs-validation'); var validation = Array.prototype.filter.call(forms, function(form) { form.addEventListener('submit', function(event) { if (form.checkValidity() === false) { var formElements = form.getElementsByClassName('input'); Array.prototype.filter.call(formElements, function(element) { if (!element.validity.valid) { setInvalid(element); } else { setValid(element) } }); event.preventDefault(); event.stopPropagation(); } form.classList.add('was-validated'); }, false); }); }; var init = function() { initFormValidation(); initPasswordVerification(); }; return { init: init } }(jQuery)); var App = (function($) { var initMenu = function() { $(".navbar-burger").on('click', function() { $(".navbar-burger").toggleClass("is-active"); $(".navbar-menu").toggleClass("is-active"); }); }; var initRepositorySwitcher = function() { $('#RepositorySelection').on('change', function() { var selectedRepoKey = $(this).find('option:selected').val(); window.location.href = '/?key=' + selectedRepoKey; }); }; var initSwitchables = function() { var $body = $('body'); $body.on('click', '.has-switchable .switchable.plaintext', function() { $(this).parents('.has-switchable').addClass('is-active'); }); $body.on('click', '.has-switchable .switchable.field .cancel-switchable', function() { var $parent = $(this).parents('.has-switchable'); var $editableField = $parent.find('.switchable.field').find('.input'); $editableField.val($editableField.data('original-value')); $parent.removeClass('is-active'); }); $body.on('click', '.has-switchable .switchable.field .is-accept-switchable', function() { var $parent = $(this).parents('.has-switchable'); var $editableField = $parent.find('.switchable.field').find('.input'); var $plainTextField = $parent.find('.switchable.plaintext'); $plainTextField.text($editableField.val()); $parent.removeClass('is-active'); }); }; var initDropdowns = function() { $('.dropdown-trigger').on('click', function() { $(this).parents('.dropdown').toggleClass('is-active'); }); }; var initNotifications = function() { $('#Notifications').on('click', '.delete', function() { $(this).parents('.notification').remove(); }); }; var initModals = function() { $('.is-modal-trigger').on('click', function() { $($(this).data('modal')).addClass('is-active'); }); $('.is-modal-cancel').on('click', function() { $(this).parents('.modal').removeClass('is-active'); }); }; var initTabs = function() { $('.tabs').each(function (i, tab) { var $tab = $(tab); var $items = $tab.find('ul li'); var $content = $($tab.data('tabs-for')); $items.each(function(j, item) { var $item = $(item); $item.on('click', function() { $items.removeClass('is-active'); $content.find('.tab-content').removeClass('is-active'); $item.addClass('is-active'); $($item.data('tab-for')).addClass('is-active'); }); }); }); }; var init = function() { initRepositorySwitcher(); initSwitchables(); initDropdowns(); initNotifications(); initMenu(); initModals(); initTabs(); }; return { init: init } }(jQuery)); var Search = (function($) { var $ImageTable; var $SearchImages; var performSearch = function() { var $searchBox = $(this); var currentSearch = $.trim($searchBox.val()).toLowerCase(); var rows = $ImageTable.find('tbody tr'); rows.each(function(i, row) { var $row = $(row); var imageName = $row.data('image-name').toLowerCase(); if (imageName.includes(currentSearch) || currentSearch.length === 0) { $row.show(); } else { $row.hide(); } }); }; var init = function() { $SearchImages = $('#SearchImages'); $ImageTable = $('#ImageTable'); $SearchImages.on('keyup', performSearch); }; return { init: init } }(jQuery)); var PullChart = (function($) { var DATE_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; var formatNumber = function(num) { var numericalNum = parseInt(num); if (numericalNum > 1000000) { return (num / 1000000) + 'm'; } else if (numericalNum > 1000) { return (num / 1000) + 'k'; } else { return num; } }; var formatDate = function(group) { if (group.length === 8) { var year = parseInt(group.substr(0, 4)); var month = parseInt(group.substr(4, 2)); var day = parseInt(group.substr(6, 2)); var date = new Date(year, month - 1, day); return date.getDate() + ' ' + DATE_MONTHS[date.getMonth()]; } return group; }; var populateChart = function(imageKey, groupMode) { Ajax.get('/internalapi/image/stats', { 'imageKey': imageKey, 'groupMode': groupMode }, function(history) { var ctx = document.getElementById('ImagePullHistory').getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: history.pullDifferential.labels, datasets: [ { lineTension: 0, data: history.pullDifferential.pulls, pointRadius: 0, pointHitRadius: 6, borderWidth: 1.5, borderColor: 'rgba(33, 96, 196, 0.7)', backgroundColor : 'rgba(33, 96, 196, 0.1)' } ] }, options: { responsive: true, maintainAspectRatio: false, legend: { display: false }, scales: { fill: false, xAxes: [ { gridLines: { display: false }, display: true, ticks: { callback: function(label) { return formatDate(label); } } } ], yAxes: [ { gridLines: { display: false }, ticks: { callback: function(label) { return formatNumber(label); } } } ] } } }); }); }; return { populateChart: populateChart } }(jQuery)); ================================================ FILE: src/main/resources/static/assets/js/fontawesome-all.js ================================================ /*! * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ !function(){"use strict";var c={},l={};try{"undefined"!=typeof window&&(c=window),"undefined"!=typeof document&&(l=document)}catch(c){}var h=(c.navigator||{}).userAgent,z=void 0===h?"":h,v=c,a=l,m=(v.document,!!a.documentElement&&!!a.head&&"function"==typeof a.addEventListener&&a.createElement,~z.indexOf("MSIE")||z.indexOf("Trident/"),"___FONT_AWESOME___"),s=function(){try{return!0}catch(c){return!1}}();var e=v||{};e[m]||(e[m]={}),e[m].styles||(e[m].styles={}),e[m].hooks||(e[m].hooks={}),e[m].shims||(e[m].shims=[]);var t=e[m];function M(c,z){var l=(2>>0;h--;)l[h]=c[h];return l}function gc(c){return c.classList?bc(c.classList):(c.getAttribute("class")||"").split(" ").filter(function(c){return c})}function Ac(c,l){var h,z=l.split("-"),v=z[0],a=z.slice(1).join("-");return v!==c||""===a||(h=a,~T.indexOf(h))?null:a}function Sc(c){return"".concat(c).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function yc(h){return Object.keys(h||{}).reduce(function(c,l){return c+"".concat(l,": ").concat(h[l],";")},"")}function wc(c){return c.size!==Lc.size||c.x!==Lc.x||c.y!==Lc.y||c.rotate!==Lc.rotate||c.flipX||c.flipY}function kc(c){var l=c.transform,h=c.containerWidth,z=c.iconWidth,v={transform:"translate(".concat(h/2," 256)")},a="translate(".concat(32*l.x,", ").concat(32*l.y,") "),m="scale(".concat(l.size/16*(l.flipX?-1:1),", ").concat(l.size/16*(l.flipY?-1:1),") "),s="rotate(".concat(l.rotate," 0 0)");return{outer:v,inner:{transform:"".concat(a," ").concat(m," ").concat(s)},path:{transform:"translate(".concat(z/2*-1," -256)")}}}var Zc={x:0,y:0,width:"100%",height:"100%"};function xc(c){var l=!(1").concat(m.map(Jc).join(""),"")}var $c=function(){};function cl(c){return"string"==typeof(c.getAttribute?c.getAttribute(J):null)}var ll={replace:function(c){var l=c[0],h=c[1].map(function(c){return Jc(c)}).join("\n");if(l.parentNode&&l.outerHTML)l.outerHTML=h+($.keepOriginalSource&&"svg"!==l.tagName.toLowerCase()?"\x3c!-- ".concat(l.outerHTML," --\x3e"):"");else if(l.parentNode){var z=document.createElement("span");l.parentNode.replaceChild(z,l),z.outerHTML=h}},nest:function(c){var l=c[0],h=c[1];if(~gc(l).indexOf($.replacementClass))return ll.replace(c);var z=new RegExp("".concat($.familyPrefix,"-.*"));delete h[0].attributes.style,delete h[0].attributes.id;var v=h[0].attributes.class.split(" ").reduce(function(c,l){return l===$.replacementClass||l.match(z)?c.toSvg.push(l):c.toNode.push(l),c},{toNode:[],toSvg:[]});h[0].attributes.class=v.toSvg.join(" ");var a=h.map(function(c){return Jc(c)}).join("\n");l.setAttribute("class",v.toNode.join(" ")),l.setAttribute(J,""),l.innerHTML=a}};function hl(c){c()}function zl(h,c){var z="function"==typeof c?c:$c;if(0===h.length)z();else{var l=hl;$.mutateApproach===y&&(l=V.requestAnimationFrame||hl),l(function(){var c=!0===$.autoReplaceSvg?ll.replace:ll[$.autoReplaceSvg]||ll.replace,l=_c.begin("mutate");h.map(c),l(),z()})}}var vl=!1;function al(){vl=!1}var ml=null;function sl(c){if(t&&$.observeMutations){var v=c.treeCallback,a=c.nodeCallback,m=c.pseudoElementsCallback,l=c.observeMutationsRoot,h=void 0===l?o:l;ml=new t(function(c){vl||bc(c).forEach(function(c){if("childList"===c.type&&0 // class from cssIcon onRenderTemplate : null, // function( index, template ) { return template; }, // template is a string onRenderHeader : null, // function( index ) {}, // nothing to return // *** functionality cancelSelection : true, // prevent text selection in the header tabIndex : true, // add tabindex to header for keyboard accessibility dateFormat : 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd' sortMultiSortKey : 'shiftKey', // key used to select additional columns sortResetKey : 'ctrlKey', // key used to remove sorting on a column usNumberFormat : true, // false for German '1.234.567,89' or French '1 234 567,89' delayInit : false, // if false, the parsed table contents will not update until the first sort serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used. resort : true, // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed // *** sort options headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. ignoreCase : true, // ignore case while sorting sortForce : null, // column(s) first sorted; always applied sortList : [], // Initial sort order; applied initially; updated when manually sorted sortAppend : null, // column(s) sorted last; always applied sortStable : false, // when sorting two rows with exactly the same content, the original sort order is maintained sortInitialOrder : 'asc', // sort direction on first click sortLocaleCompare: false, // replace equivalent character (accented characters) sortReset : false, // third click on the header will reset column to default - unsorted sortRestart : false, // restart sort to 'sortInitialOrder' when clicking on previously unsorted columns emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero duplicateSpan : true, // colspan cells in the tbody will have duplicated content in the cache for each spanned column textExtraction : 'basic', // text extraction method/function - function( node, table, cellIndex ) {} textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in default textExtraction function) textSorter : null, // choose overall or specific column sorter function( a, b, direction, table, columnIndex ) [alt: ts.sortText] numberSorter : null, // choose overall numeric sorter function( a, b, direction, maxColumnValue ) // *** widget options initWidgets : true, // apply widgets on tablesorter initialization widgetClass : 'widget-{name}', // table class name template to match to include a widget widgets : [], // method to add widgets, e.g. widgets: ['zebra'] widgetOptions : { zebra : [ 'even', 'odd' ] // zebra widget alternating row class names }, // *** callbacks initialized : null, // function( table ) {}, // *** extra css class names tableClass : '', cssAsc : '', cssDesc : '', cssNone : '', cssHeader : '', cssHeaderRow : '', cssProcessing : '', // processing icon applied to header during sort/filter cssChildRow : 'tablesorter-childRow', // class name indiciating that a row is to be attached to its parent cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!) cssNoSort : 'tablesorter-noSort', // class name added to element inside header; clicking on it won't cause a sort cssIgnoreRow : 'tablesorter-ignoreRow',// header row to ignore; cells within this row will not be added to c.$headers cssIcon : 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate cssIconNone : '', // class name added to the icon when there is no column sort cssIconAsc : '', // class name added to the icon when the column has an ascending sort cssIconDesc : '', // class name added to the icon when the column has a descending sort cssIconDisabled : '', // class name added to the icon when the column has a disabled sort // *** events pointerClick : 'click', pointerDown : 'mousedown', pointerUp : 'mouseup', // *** selectors selectorHeaders : '> thead th, > thead td', selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort selectorRemove : '.remove-me', // *** advanced debug : false, // *** Internal variables headerList: [], empties: {}, strings: {}, parsers: [], // *** parser options for validator; values must be falsy! globalize: 0, imgAttr: 0 // removed: widgetZebra: { css: ['even', 'odd'] } }, // internal css classes - these will ALWAYS be added to // the table and MUST only contain one class name - fixes #381 css : { table : 'tablesorter', cssHasChild: 'tablesorter-hasChildRow', childRow : 'tablesorter-childRow', colgroup : 'tablesorter-colgroup', header : 'tablesorter-header', headerRow : 'tablesorter-headerRow', headerIn : 'tablesorter-header-inner', icon : 'tablesorter-icon', processing : 'tablesorter-processing', sortAsc : 'tablesorter-headerAsc', sortDesc : 'tablesorter-headerDesc', sortNone : 'tablesorter-headerUnSorted' }, // labels applied to sortable headers for accessibility (aria) support language : { sortAsc : 'Ascending sort applied, ', sortDesc : 'Descending sort applied, ', sortNone : 'No sort applied, ', sortDisabled : 'sorting is disabled', nextAsc : 'activate to apply an ascending sort', nextDesc : 'activate to apply a descending sort', nextNone : 'activate to remove the sort' }, regex : { templateContent : /\{content\}/g, templateIcon : /\{icon\}/g, templateName : /\{name\}/i, spaces : /\s+/g, nonWord : /\W/g, formElements : /(input|select|button|textarea)/i, // *** sort functions *** // regex used in natural sort // chunk/tokenize numbers & letters chunk : /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, // replace chunks @ ends chunks : /(^\\0|\\0$)/, hex : /^0x[0-9a-f]+$/i, // *** formatFloat *** comma : /,/g, digitNonUS : /[\s|\.]/g, digitNegativeTest : /^\s*\([.\d]+\)/, digitNegativeReplace : /^\s*\(([.\d]+)\)/, // *** isDigit *** digitTest : /^[\-+(]?\d+[)]?$/, digitReplace : /[,.'"\s]/g }, // digit sort, text location string : { max : 1, min : -1, emptymin : 1, emptymax : -1, zero : 0, none : 0, 'null' : 0, top : true, bottom : false }, keyCodes : { enter : 13 }, // placeholder date parser data (globalize) dates : {}, // These methods can be applied on table.config instance instanceMethods : {}, /* ▄█████ ██████ ██████ ██ ██ █████▄ ▀█▄ ██▄▄ ██ ██ ██ ██▄▄██ ▀█▄ ██▀▀ ██ ██ ██ ██▀▀▀ █████▀ ██████ ██ ▀████▀ ██ */ setup : function( table, c ) { // if no thead or tbody, or tablesorter is already present, quit if ( !table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true ) { if ( ts.debug(c, 'core') ) { if ( table.hasInitialized ) { console.warn( 'Stopping initialization. Tablesorter has already been initialized' ); } else { console.error( 'Stopping initialization! No table, thead or tbody', table ); } } return; } var tmp = '', $table = $( table ), meta = $.metadata; // initialization flag table.hasInitialized = false; // table is being processed flag table.isProcessing = true; // make sure to store the config object table.config = c; // save the settings where they read $.data( table, 'tablesorter', c ); if ( ts.debug(c, 'core') ) { console[ console.group ? 'group' : 'log' ]( 'Initializing tablesorter v' + ts.version ); $.data( table, 'startoveralltimer', new Date() ); } // removing this in version 3 (only supports jQuery 1.7+) c.supportsDataObject = ( function( version ) { version[ 0 ] = parseInt( version[ 0 ], 10 ); return ( version[ 0 ] > 1 ) || ( version[ 0 ] === 1 && parseInt( version[ 1 ], 10 ) >= 4 ); })( $.fn.jquery.split( '.' ) ); // ensure case insensitivity c.emptyTo = c.emptyTo.toLowerCase(); c.stringTo = c.stringTo.toLowerCase(); c.last = { sortList : [], clickedIndex : -1 }; // add table theme class only if there isn't already one there if ( !/tablesorter\-/.test( $table.attr( 'class' ) ) ) { tmp = ( c.theme !== '' ? ' tablesorter-' + c.theme : '' ); } // give the table a unique id, which will be used in namespace binding if ( !c.namespace ) { c.namespace = '.tablesorter' + Math.random().toString( 16 ).slice( 2 ); } else { // make sure namespace starts with a period & doesn't have weird characters c.namespace = '.' + c.namespace.replace( ts.regex.nonWord, '' ); } c.table = table; c.$table = $table // add namespace to table to allow bindings on extra elements to target // the parent table (e.g. parser-input-select) .addClass( ts.css.table + ' ' + c.tableClass + tmp + ' ' + c.namespace.slice(1) ) .attr( 'role', 'grid' ); c.$headers = $table.find( c.selectorHeaders ); c.$table.children().children( 'tr' ).attr( 'role', 'row' ); c.$tbodies = $table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ).attr({ 'aria-live' : 'polite', 'aria-relevant' : 'all' }); if ( c.$table.children( 'caption' ).length ) { tmp = c.$table.children( 'caption' )[ 0 ]; if ( !tmp.id ) { tmp.id = c.namespace.slice( 1 ) + 'caption'; } c.$table.attr( 'aria-labelledby', tmp.id ); } c.widgetInit = {}; // keep a list of initialized widgets // change textExtraction via data-attribute c.textExtraction = c.$table.attr( 'data-text-extraction' ) || c.textExtraction || 'basic'; // build headers ts.buildHeaders( c ); // fixate columns if the users supplies the fixedWidth option // do this after theme has been applied ts.fixColumnWidth( table ); // add widgets from class name ts.addWidgetFromClass( table ); // add widget options before parsing (e.g. grouping widget has parser settings) ts.applyWidgetOptions( table ); // try to auto detect column type, and store in tables config ts.setupParsers( c ); // start total row count at zero c.totalRows = 0; // only validate options while debugging. See #1528 if (c.debug) { ts.validateOptions( c ); } // build the cache for the tbody cells // delayInit will delay building the cache until the user starts a sort if ( !c.delayInit ) { ts.buildCache( c ); } // bind all header events and methods ts.bindEvents( table, c.$headers, true ); ts.bindMethods( c ); // get sort list from jQuery data or metadata // in jQuery < 1.4, an error occurs when calling $table.data() if ( c.supportsDataObject && typeof $table.data().sortlist !== 'undefined' ) { c.sortList = $table.data().sortlist; } else if ( meta && ( $table.metadata() && $table.metadata().sortlist ) ) { c.sortList = $table.metadata().sortlist; } // apply widget init code ts.applyWidget( table, true ); // if user has supplied a sort list to constructor if ( c.sortList.length > 0 ) { // save sortList before any sortAppend is added c.last.sortList = c.sortList; ts.sortOn( c, c.sortList, {}, !c.initWidgets ); } else { ts.setHeadersCss( c ); if ( c.initWidgets ) { // apply widget format ts.applyWidget( table, false ); } } // show processesing icon if ( c.showProcessing ) { $table .unbind( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace ) .bind( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace, function( e ) { clearTimeout( c.timerProcessing ); ts.isProcessing( table ); if ( e.type === 'sortBegin' ) { c.timerProcessing = setTimeout( function() { ts.isProcessing( table, true ); }, 500 ); } }); } // initialized table.hasInitialized = true; table.isProcessing = false; if ( ts.debug(c, 'core') ) { console.log( 'Overall initialization time:' + ts.benchmark( $.data( table, 'startoveralltimer' ) ) ); if ( ts.debug(c, 'core') && console.groupEnd ) { console.groupEnd(); } } $table.triggerHandler( 'tablesorter-initialized', table ); if ( typeof c.initialized === 'function' ) { c.initialized( table ); } }, bindMethods : function( c ) { var $table = c.$table, namespace = c.namespace, events = ( 'sortReset update updateRows updateAll updateHeaders addRows updateCell updateComplete ' + 'sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup ' + 'mouseleave ' ).split( ' ' ) .join( namespace + ' ' ); // apply easy methods that trigger bound events $table .unbind( events.replace( ts.regex.spaces, ' ' ) ) .bind( 'sortReset' + namespace, function( e, callback ) { e.stopPropagation(); // using this.config to ensure functions are getting a non-cached version of the config ts.sortReset( this.config, function( table ) { if (table.isApplyingWidgets) { // multiple triggers in a row... filterReset, then sortReset - see #1361 // wait to update widgets setTimeout( function() { ts.applyWidget( table, '', callback ); }, 100 ); } else { ts.applyWidget( table, '', callback ); } }); }) .bind( 'updateAll' + namespace, function( e, resort, callback ) { e.stopPropagation(); ts.updateAll( this.config, resort, callback ); }) .bind( 'update' + namespace + ' updateRows' + namespace, function( e, resort, callback ) { e.stopPropagation(); ts.update( this.config, resort, callback ); }) .bind( 'updateHeaders' + namespace, function( e, callback ) { e.stopPropagation(); ts.updateHeaders( this.config, callback ); }) .bind( 'updateCell' + namespace, function( e, cell, resort, callback ) { e.stopPropagation(); ts.updateCell( this.config, cell, resort, callback ); }) .bind( 'addRows' + namespace, function( e, $row, resort, callback ) { e.stopPropagation(); ts.addRows( this.config, $row, resort, callback ); }) .bind( 'updateComplete' + namespace, function() { this.isUpdating = false; }) .bind( 'sorton' + namespace, function( e, list, callback, init ) { e.stopPropagation(); ts.sortOn( this.config, list, callback, init ); }) .bind( 'appendCache' + namespace, function( e, callback, init ) { e.stopPropagation(); ts.appendCache( this.config, init ); if ( $.isFunction( callback ) ) { callback( this ); } }) // $tbodies variable is used by the tbody sorting widget .bind( 'updateCache' + namespace, function( e, callback, $tbodies ) { e.stopPropagation(); ts.updateCache( this.config, callback, $tbodies ); }) .bind( 'applyWidgetId' + namespace, function( e, id ) { e.stopPropagation(); ts.applyWidgetId( this, id ); }) .bind( 'applyWidgets' + namespace, function( e, callback ) { e.stopPropagation(); // apply widgets (false = not initializing) ts.applyWidget( this, false, callback ); }) .bind( 'refreshWidgets' + namespace, function( e, all, dontapply ) { e.stopPropagation(); ts.refreshWidgets( this, all, dontapply ); }) .bind( 'removeWidget' + namespace, function( e, name, refreshing ) { e.stopPropagation(); ts.removeWidget( this, name, refreshing ); }) .bind( 'destroy' + namespace, function( e, removeClasses, callback ) { e.stopPropagation(); ts.destroy( this, removeClasses, callback ); }) .bind( 'resetToLoadState' + namespace, function( e ) { e.stopPropagation(); // remove all widgets ts.removeWidget( this, true, false ); var tmp = $.extend( true, {}, c.originalSettings ); // restore original settings; this clears out current settings, but does not clear // values saved to storage. c = $.extend( true, {}, ts.defaults, tmp ); c.originalSettings = tmp; this.hasInitialized = false; // setup the entire table again ts.setup( this, c ); }); }, bindEvents : function( table, $headers, core ) { table = $( table )[ 0 ]; var tmp, c = table.config, namespace = c.namespace, downTarget = null; if ( core !== true ) { $headers.addClass( namespace.slice( 1 ) + '_extra_headers' ); tmp = ts.getClosest( $headers, 'table' ); if ( tmp.length && tmp[ 0 ].nodeName === 'TABLE' && tmp[ 0 ] !== table ) { $( tmp[ 0 ] ).addClass( namespace.slice( 1 ) + '_extra_table' ); } } tmp = ( c.pointerDown + ' ' + c.pointerUp + ' ' + c.pointerClick + ' sort keyup ' ) .replace( ts.regex.spaces, ' ' ) .split( ' ' ) .join( namespace + ' ' ); // apply event handling to headers and/or additional headers (stickyheaders, scroller, etc) $headers // http://stackoverflow.com/questions/5312849/jquery-find-self; .find( c.selectorSort ) .add( $headers.filter( c.selectorSort ) ) .unbind( tmp ) .bind( tmp, function( e, external ) { var $cell, cell, temp, $target = $( e.target ), // wrap event type in spaces, so the match doesn't trigger on inner words type = ' ' + e.type + ' '; // only recognize left clicks if ( ( ( e.which || e.button ) !== 1 && !type.match( ' ' + c.pointerClick + ' | sort | keyup ' ) ) || // allow pressing enter ( type === ' keyup ' && e.which !== ts.keyCodes.enter ) || // allow triggering a click event (e.which is undefined) & ignore physical clicks ( type.match( ' ' + c.pointerClick + ' ' ) && typeof e.which !== 'undefined' ) ) { return; } // ignore mouseup if mousedown wasn't on the same target if ( type.match( ' ' + c.pointerUp + ' ' ) && downTarget !== e.target && external !== true ) { return; } // set target on mousedown if ( type.match( ' ' + c.pointerDown + ' ' ) ) { downTarget = e.target; // preventDefault needed or jQuery v1.3.2 and older throws an // "Uncaught TypeError: handler.apply is not a function" error temp = $target.jquery.split( '.' ); if ( temp[ 0 ] === '1' && temp[ 1 ] < 4 ) { e.preventDefault(); } return; } downTarget = null; $cell = ts.getClosest( $( this ), '.' + ts.css.header ); // prevent sort being triggered on form elements if ( ts.regex.formElements.test( e.target.nodeName ) || // nosort class name, or elements within a nosort container $target.hasClass( c.cssNoSort ) || $target.parents( '.' + c.cssNoSort ).length > 0 || // disabled cell directly clicked $cell.hasClass( 'sorter-false' ) || // elements within a button $target.parents( 'button' ).length > 0 ) { return !c.cancelSelection; } if ( c.delayInit && ts.isEmptyObject( c.cache ) ) { ts.buildCache( c ); } // use column index from data-attribute or index of current row; fixes #1116 c.last.clickedIndex = $cell.attr( 'data-column' ) || $cell.index(); cell = c.$headerIndexed[ c.last.clickedIndex ][0]; if ( cell && !cell.sortDisabled ) { ts.initSort( c, cell, e ); } }); if ( c.cancelSelection ) { // cancel selection $headers .attr( 'unselectable', 'on' ) .bind( 'selectstart', false ) .css({ 'user-select' : 'none', 'MozUserSelect' : 'none' // not needed for jQuery 1.8+ }); } }, buildHeaders : function( c ) { var $temp, icon, timer, indx; c.headerList = []; c.headerContent = []; c.sortVars = []; if ( ts.debug(c, 'core') ) { timer = new Date(); } // children tr in tfoot - see issue #196 & #547 // don't pass table.config to computeColumnIndex here - widgets (math) pass it to "quickly" index tbody cells c.columns = ts.computeColumnIndex( c.$table.children( 'thead, tfoot' ).children( 'tr' ) ); // add icon if cssIcon option exists icon = c.cssIcon ? '' : ''; // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683 c.$headers = $( $.map( c.$table.find( c.selectorHeaders ), function( elem, index ) { var configHeaders, header, column, template, tmp, $elem = $( elem ); // ignore cell (don't add it to c.$headers) if row has ignoreRow class if ( ts.getClosest( $elem, 'tr' ).hasClass( c.cssIgnoreRow ) ) { return; } // transfer data-column to element if not th/td - #1459 if ( !/(th|td)/i.test( elem.nodeName ) ) { tmp = ts.getClosest( $elem, 'th, td' ); $elem.attr( 'data-column', tmp.attr( 'data-column' ) ); } // make sure to get header cell & not column indexed cell configHeaders = ts.getColumnData( c.table, c.headers, index, true ); // save original header content c.headerContent[ index ] = $elem.html(); // if headerTemplate is empty, don't reformat the header cell if ( c.headerTemplate !== '' && !$elem.find( '.' + ts.css.headerIn ).length ) { // set up header template template = c.headerTemplate .replace( ts.regex.templateContent, $elem.html() ) .replace( ts.regex.templateIcon, $elem.find( '.' + ts.css.icon ).length ? '' : icon ); if ( c.onRenderTemplate ) { header = c.onRenderTemplate.apply( $elem, [ index, template ] ); // only change t if something is returned if ( header && typeof header === 'string' ) { template = header; } } $elem.html( '
' + template + '
' ); // faster than wrapInner } if ( c.onRenderHeader ) { c.onRenderHeader.apply( $elem, [ index, c, c.$table ] ); } column = parseInt( $elem.attr( 'data-column' ), 10 ); elem.column = column; tmp = ts.getOrder( ts.getData( $elem, configHeaders, 'sortInitialOrder' ) || c.sortInitialOrder ); // this may get updated numerous times if there are multiple rows c.sortVars[ column ] = { count : -1, // set to -1 because clicking on the header automatically adds one order : tmp ? ( c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ] ) : // desc, asc, unsorted ( c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ] ), // asc, desc, unsorted lockedOrder : false, sortedBy : '' }; tmp = ts.getData( $elem, configHeaders, 'lockedOrder' ) || false; if ( typeof tmp !== 'undefined' && tmp !== false ) { c.sortVars[ column ].lockedOrder = true; c.sortVars[ column ].order = ts.getOrder( tmp ) ? [ 1, 1 ] : [ 0, 0 ]; } // add cell to headerList c.headerList[ index ] = elem; $elem.addClass( ts.css.header + ' ' + c.cssHeader ); // add to parent in case there are multiple rows ts.getClosest( $elem, 'tr' ) .addClass( ts.css.headerRow + ' ' + c.cssHeaderRow ) .attr( 'role', 'row' ); // allow keyboard cursor to focus on element if ( c.tabIndex ) { $elem.attr( 'tabindex', 0 ); } return elem; }) ); // cache headers per column c.$headerIndexed = []; for ( indx = 0; indx < c.columns; indx++ ) { // colspan in header making a column undefined if ( ts.isEmptyObject( c.sortVars[ indx ] ) ) { c.sortVars[ indx ] = {}; } // Use c.$headers.parent() in case selectorHeaders doesn't point to the th/td $temp = c.$headers.filter( '[data-column="' + indx + '"]' ); // target sortable column cells, unless there are none, then use non-sortable cells // .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6 c.$headerIndexed[ indx ] = $temp.length ? $temp.not( '.sorter-false' ).length ? $temp.not( '.sorter-false' ).filter( ':last' ) : $temp.filter( ':last' ) : $(); } c.$table.find( c.selectorHeaders ).attr({ scope: 'col', role : 'columnheader' }); // enable/disable sorting ts.updateHeader( c ); if ( ts.debug(c, 'core') ) { console.log( 'Built headers:' + ts.benchmark( timer ) ); console.log( c.$headers ); } }, // Use it to add a set of methods to table.config which will be available for all tables. // This should be done before table initialization addInstanceMethods : function( methods ) { $.extend( ts.instanceMethods, methods ); }, /* █████▄ ▄████▄ █████▄ ▄█████ ██████ █████▄ ▄█████ ██▄▄██ ██▄▄██ ██▄▄██ ▀█▄ ██▄▄ ██▄▄██ ▀█▄ ██▀▀▀ ██▀▀██ ██▀██ ▀█▄ ██▀▀ ██▀██ ▀█▄ ██ ██ ██ ██ ██ █████▀ ██████ ██ ██ █████▀ */ setupParsers : function( c, $tbodies ) { var rows, list, span, max, colIndex, indx, header, configHeaders, noParser, parser, extractor, time, tbody, len, table = c.table, tbodyIndex = 0, debug = ts.debug(c, 'core'), debugOutput = {}; // update table bodies in case we start with an empty table c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ); tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies; len = tbody.length; if ( len === 0 ) { return debug ? console.warn( 'Warning: *Empty table!* Not building a parser cache' ) : ''; } else if ( debug ) { time = new Date(); console[ console.group ? 'group' : 'log' ]( 'Detecting parsers for each column' ); } list = { extractors: [], parsers: [] }; while ( tbodyIndex < len ) { rows = tbody[ tbodyIndex ].rows; if ( rows.length ) { colIndex = 0; max = c.columns; for ( indx = 0; indx < max; indx++ ) { header = c.$headerIndexed[ colIndex ]; if ( header && header.length ) { // get column indexed table cell; adding true parameter fixes #1362 but // it would break backwards compatibility... configHeaders = ts.getColumnData( table, c.headers, colIndex ); // , true ); // get column parser/extractor extractor = ts.getParserById( ts.getData( header, configHeaders, 'extractor' ) ); parser = ts.getParserById( ts.getData( header, configHeaders, 'sorter' ) ); noParser = ts.getData( header, configHeaders, 'parser' ) === 'false'; // empty cells behaviour - keeping emptyToBottom for backwards compatibility c.empties[colIndex] = ( ts.getData( header, configHeaders, 'empty' ) || c.emptyTo || ( c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase(); // text strings behaviour in numerical sorts c.strings[colIndex] = ( ts.getData( header, configHeaders, 'string' ) || c.stringTo || 'max' ).toLowerCase(); if ( noParser ) { parser = ts.getParserById( 'no-parser' ); } if ( !extractor ) { // For now, maybe detect someday extractor = false; } if ( !parser ) { parser = ts.detectParserForColumn( c, rows, -1, colIndex ); } if ( debug ) { debugOutput[ '(' + colIndex + ') ' + header.text() ] = { parser : parser.id, extractor : extractor ? extractor.id : 'none', string : c.strings[ colIndex ], empty : c.empties[ colIndex ] }; } list.parsers[ colIndex ] = parser; list.extractors[ colIndex ] = extractor; span = header[ 0 ].colSpan - 1; if ( span > 0 ) { colIndex += span; max += span; while ( span + 1 > 0 ) { // set colspan columns to use the same parsers & extractors list.parsers[ colIndex - span ] = parser; list.extractors[ colIndex - span ] = extractor; span--; } } } colIndex++; } } tbodyIndex += ( list.parsers.length ) ? len : 1; } if ( debug ) { if ( !ts.isEmptyObject( debugOutput ) ) { console[ console.table ? 'table' : 'log' ]( debugOutput ); } else { console.warn( ' No parsers detected!' ); } console.log( 'Completed detecting parsers' + ts.benchmark( time ) ); if ( console.groupEnd ) { console.groupEnd(); } } c.parsers = list.parsers; c.extractors = list.extractors; }, addParser : function( parser ) { var indx, len = ts.parsers.length, add = true; for ( indx = 0; indx < len; indx++ ) { if ( ts.parsers[ indx ].id.toLowerCase() === parser.id.toLowerCase() ) { add = false; } } if ( add ) { ts.parsers[ ts.parsers.length ] = parser; } }, getParserById : function( name ) { /*jshint eqeqeq:false */ // eslint-disable-next-line eqeqeq if ( name == 'false' ) { return false; } var indx, len = ts.parsers.length; for ( indx = 0; indx < len; indx++ ) { if ( ts.parsers[ indx ].id.toLowerCase() === ( name.toString() ).toLowerCase() ) { return ts.parsers[ indx ]; } } return false; }, detectParserForColumn : function( c, rows, rowIndex, cellIndex ) { var cur, $node, row, indx = ts.parsers.length, node = false, nodeValue = '', debug = ts.debug(c, 'core'), keepLooking = true; while ( nodeValue === '' && keepLooking ) { rowIndex++; row = rows[ rowIndex ]; // stop looking after 50 empty rows if ( row && rowIndex < 50 ) { if ( row.className.indexOf( ts.cssIgnoreRow ) < 0 ) { node = rows[ rowIndex ].cells[ cellIndex ]; nodeValue = ts.getElementText( c, node, cellIndex ); $node = $( node ); if ( debug ) { console.log( 'Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': "' + nodeValue + '"' ); } } } else { keepLooking = false; } } while ( --indx >= 0 ) { cur = ts.parsers[ indx ]; // ignore the default text parser because it will always be true if ( cur && cur.id !== 'text' && cur.is && cur.is( nodeValue, c.table, node, $node ) ) { return cur; } } // nothing found, return the generic parser (text) return ts.getParserById( 'text' ); }, getElementText : function( c, node, cellIndex ) { if ( !node ) { return ''; } var tmp, extract = c.textExtraction || '', // node could be a jquery object // http://jsperf.com/jquery-vs-instanceof-jquery/2 $node = node.jquery ? node : $( node ); if ( typeof extract === 'string' ) { // check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow! // http://www.kellegous.com/j/2013/02/27/innertext-vs-textcontent/ if ( extract === 'basic' && typeof ( tmp = $node.attr( c.textAttribute ) ) !== 'undefined' ) { return $.trim( tmp ); } return $.trim( node.textContent || $node.text() ); } else { if ( typeof extract === 'function' ) { return $.trim( extract( $node[ 0 ], c.table, cellIndex ) ); } else if ( typeof ( tmp = ts.getColumnData( c.table, extract, cellIndex ) ) === 'function' ) { return $.trim( tmp( $node[ 0 ], c.table, cellIndex ) ); } } // fallback return $.trim( $node[ 0 ].textContent || $node.text() ); }, // centralized function to extract/parse cell contents getParsedText : function( c, cell, colIndex, txt ) { if ( typeof txt === 'undefined' ) { txt = ts.getElementText( c, cell, colIndex ); } // if no parser, make sure to return the txt var val = '' + txt, parser = c.parsers[ colIndex ], extractor = c.extractors[ colIndex ]; if ( parser ) { // do extract before parsing, if there is one if ( extractor && typeof extractor.format === 'function' ) { txt = extractor.format( txt, c.table, cell, colIndex ); } // allow parsing if the string is empty, previously parsing would change it to zero, // in case the parser needs to extract data from the table cell attributes val = parser.id === 'no-parser' ? '' : // make sure txt is a string (extractor may have converted it) parser.format( '' + txt, c.table, cell, colIndex ); if ( c.ignoreCase && typeof val === 'string' ) { val = val.toLowerCase(); } } return val; }, /* ▄████▄ ▄████▄ ▄████▄ ██ ██ ██████ ██ ▀▀ ██▄▄██ ██ ▀▀ ██▄▄██ ██▄▄ ██ ▄▄ ██▀▀██ ██ ▄▄ ██▀▀██ ██▀▀ ▀████▀ ██ ██ ▀████▀ ██ ██ ██████ */ buildCache : function( c, callback, $tbodies ) { var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row, cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData, colMax, span, cacheIndex, hasParser, max, len, index, table = c.table, parsers = c.parsers, debug = ts.debug(c, 'core'); // update tbody variable c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ); $tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies, c.cache = {}; c.totalRows = 0; // if no parsers found, return - it's an empty table. if ( !parsers ) { return debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : ''; } if ( debug ) { cacheTime = new Date(); } // processing icon if ( c.showProcessing ) { ts.isProcessing( table, true ); } for ( tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++ ) { colMax = []; // column max value per tbody cache = c.cache[ tbodyIndex ] = { normalized: [] // array of normalized row data; last entry contains 'rowData' above // colMax: # // added at the end }; totalRows = ( $tbody[ tbodyIndex ] && $tbody[ tbodyIndex ].rows.length ) || 0; for ( rowIndex = 0; rowIndex < totalRows; ++rowIndex ) { rowData = { // order: original row order # // $row : jQuery Object[] child: [], // child row text (filter widget) raw: [] // original row text }; /** Add the table data to main data array */ $row = $( $tbody[ tbodyIndex ].rows[ rowIndex ] ); cols = []; // ignore "remove-me" rows if ( $row.hasClass( c.selectorRemove.slice(1) ) ) { continue; } // if this is a child row, add it to the last row's children and continue to the next row // ignore child row class, if it is the first row if ( $row.hasClass( c.cssChildRow ) && rowIndex !== 0 ) { len = cache.normalized.length - 1; prevRowData = cache.normalized[ len ][ c.columns ]; prevRowData.$row = prevRowData.$row.add( $row ); // add 'hasChild' class name to parent row if ( !$row.prev().hasClass( c.cssChildRow ) ) { $row.prev().addClass( ts.css.cssHasChild ); } // save child row content (un-parsed!) $cells = $row.children( 'th, td' ); len = prevRowData.child.length; prevRowData.child[ len ] = []; // child row content does not account for colspans/rowspans; so indexing may be off cacheIndex = 0; max = c.columns; for ( colIndex = 0; colIndex < max; colIndex++ ) { cell = $cells[ colIndex ]; if ( cell ) { prevRowData.child[ len ][ colIndex ] = ts.getParsedText( c, cell, colIndex ); span = $cells[ colIndex ].colSpan - 1; if ( span > 0 ) { cacheIndex += span; max += span; } } cacheIndex++; } // go to the next for loop continue; } rowData.$row = $row; rowData.order = rowIndex; // add original row position to rowCache cacheIndex = 0; max = c.columns; for ( colIndex = 0; colIndex < max; ++colIndex ) { cell = $row[ 0 ].cells[ colIndex ]; if ( cell && cacheIndex < c.columns ) { hasParser = typeof parsers[ cacheIndex ] !== 'undefined'; if ( !hasParser && debug ) { console.warn( 'No parser found for row: ' + rowIndex + ', column: ' + colIndex + '; cell containing: "' + $(cell).text() + '"; does it have a header?' ); } val = ts.getElementText( c, cell, cacheIndex ); rowData.raw[ cacheIndex ] = val; // save original row text // save raw column text even if there is no parser set txt = ts.getParsedText( c, cell, cacheIndex, val ); cols[ cacheIndex ] = txt; if ( hasParser && ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) { // determine column max value (ignore sign) colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 ); } // allow colSpan in tbody span = cell.colSpan - 1; if ( span > 0 ) { index = 0; while ( index <= span ) { // duplicate text (or not) to spanned columns // instead of setting duplicate span to empty string, use textExtraction to try to get a value // see http://stackoverflow.com/q/36449711/145346 txt = c.duplicateSpan || index === 0 ? val : typeof c.textExtraction !== 'string' ? ts.getElementText( c, cell, cacheIndex + index ) || '' : ''; rowData.raw[ cacheIndex + index ] = txt; cols[ cacheIndex + index ] = txt; index++; } cacheIndex += span; max += span; } } cacheIndex++; } // ensure rowData is always in the same location (after the last column) cols[ c.columns ] = rowData; cache.normalized[ cache.normalized.length ] = cols; } cache.colMax = colMax; // total up rows, not including child rows c.totalRows += cache.normalized.length; } if ( c.showProcessing ) { ts.isProcessing( table ); // remove processing icon } if ( debug ) { len = Math.min( 5, c.cache[ 0 ].normalized.length ); console[ console.group ? 'group' : 'log' ]( 'Building cache for ' + c.totalRows + ' rows (showing ' + len + ' rows in log) and ' + c.columns + ' columns' + ts.benchmark( cacheTime ) ); val = {}; for ( colIndex = 0; colIndex < c.columns; colIndex++ ) { for ( cacheIndex = 0; cacheIndex < len; cacheIndex++ ) { if ( !val[ 'row: ' + cacheIndex ] ) { val[ 'row: ' + cacheIndex ] = {}; } val[ 'row: ' + cacheIndex ][ c.$headerIndexed[ colIndex ].text() ] = c.cache[ 0 ].normalized[ cacheIndex ][ colIndex ]; } } console[ console.table ? 'table' : 'log' ]( val ); if ( console.groupEnd ) { console.groupEnd(); } } if ( $.isFunction( callback ) ) { callback( table ); } }, getColumnText : function( table, column, callback, rowFilter ) { table = $( table )[0]; var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result, hasCallback = typeof callback === 'function', allColumns = column === 'all', data = { raw : [], parsed: [], $cell: [] }, c = table.config; if ( ts.isEmptyObject( c ) ) { if ( ts.debug(c, 'core') ) { console.warn( 'No cache found - aborting getColumnText function!' ); } } else { tbodyLen = c.$tbodies.length; for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) { cache = c.cache[ tbodyIndex ].normalized; rowLen = cache.length; for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) { row = cache[ rowIndex ]; if ( rowFilter && !row[ c.columns ].$row.is( rowFilter ) ) { continue; } result = true; parsed = ( allColumns ) ? row.slice( 0, c.columns ) : row[ column ]; row = row[ c.columns ]; raw = ( allColumns ) ? row.raw : row.raw[ column ]; $cell = ( allColumns ) ? row.$row.children() : row.$row.children().eq( column ); if ( hasCallback ) { result = callback({ tbodyIndex : tbodyIndex, rowIndex : rowIndex, parsed : parsed, raw : raw, $row : row.$row, $cell : $cell }); } if ( result !== false ) { data.parsed[ data.parsed.length ] = parsed; data.raw[ data.raw.length ] = raw; data.$cell[ data.$cell.length ] = $cell; } } } // return everything return data; } }, /* ██ ██ █████▄ █████▄ ▄████▄ ██████ ██████ ██ ██ ██▄▄██ ██ ██ ██▄▄██ ██ ██▄▄ ██ ██ ██▀▀▀ ██ ██ ██▀▀██ ██ ██▀▀ ▀████▀ ██ █████▀ ██ ██ ██ ██████ */ setHeadersCss : function( c ) { var indx, column, list = c.sortList, len = list.length, none = ts.css.sortNone + ' ' + c.cssNone, css = [ ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc ], cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ], aria = [ 'ascending', 'descending' ], updateColumnSort = function($el, index) { $el .removeClass( none ) .addClass( css[ index ] ) .attr( 'aria-sort', aria[ index ] ) .find( '.' + ts.css.icon ) .removeClass( cssIcon[ 2 ] ) .addClass( cssIcon[ index ] ); }, // find the footer $extras = c.$table .find( 'tfoot tr' ) .children( 'td, th' ) .add( $( c.namespace + '_extra_headers' ) ) .removeClass( css.join( ' ' ) ), // remove all header information $sorted = c.$headers .add( $( 'thead ' + c.namespace + '_extra_headers' ) ) .removeClass( css.join( ' ' ) ) .addClass( none ) .attr( 'aria-sort', 'none' ) .find( '.' + ts.css.icon ) .removeClass( cssIcon.join( ' ' ) ) .end(); // add css none to all sortable headers $sorted .not( '.sorter-false' ) .find( '.' + ts.css.icon ) .addClass( cssIcon[ 2 ] ); // add disabled css icon class if ( c.cssIconDisabled ) { $sorted .filter( '.sorter-false' ) .find( '.' + ts.css.icon ) .addClass( c.cssIconDisabled ); } for ( indx = 0; indx < len; indx++ ) { // direction = 2 means reset! if ( list[ indx ][ 1 ] !== 2 ) { // multicolumn sorting updating - see #1005 // .not(function() {}) needs jQuery 1.4 // filter(function(i, el) {}) <- el is undefined in jQuery v1.2.6 $sorted = c.$headers.filter( function( i ) { // only include headers that are in the sortList (this includes colspans) var include = true, $el = c.$headers.eq( i ), col = parseInt( $el.attr( 'data-column' ), 10 ), end = col + ts.getClosest( $el, 'th, td' )[0].colSpan; for ( ; col < end; col++ ) { include = include ? include || ts.isValueInArray( col, c.sortList ) > -1 : false; } return include; }); // choose the :last in case there are nested columns $sorted = $sorted .not( '.sorter-false' ) .filter( '[data-column="' + list[ indx ][ 0 ] + '"]' + ( len === 1 ? ':last' : '' ) ); if ( $sorted.length ) { for ( column = 0; column < $sorted.length; column++ ) { if ( !$sorted[ column ].sortDisabled ) { updateColumnSort( $sorted.eq( column ), list[ indx ][ 1 ] ); } } } // add sorted class to footer & extra headers, if they exist if ( $extras.length ) { updateColumnSort( $extras.filter( '[data-column="' + list[ indx ][ 0 ] + '"]' ), list[ indx ][ 1 ] ); } } } // add verbose aria labels len = c.$headers.length; for ( indx = 0; indx < len; indx++ ) { ts.setColumnAriaLabel( c, c.$headers.eq( indx ) ); } }, getClosest : function( $el, selector ) { // jQuery v1.2.6 doesn't have closest() if ( $.fn.closest ) { return $el.closest( selector ); } return $el.is( selector ) ? $el : $el.parents( selector ).filter( ':first' ); }, // nextSort (optional), lets you disable next sort text setColumnAriaLabel : function( c, $header, nextSort ) { if ( $header.length ) { var column = parseInt( $header.attr( 'data-column' ), 10 ), vars = c.sortVars[ column ], tmp = $header.hasClass( ts.css.sortAsc ) ? 'sortAsc' : $header.hasClass( ts.css.sortDesc ) ? 'sortDesc' : 'sortNone', txt = $.trim( $header.text() ) + ': ' + ts.language[ tmp ]; if ( $header.hasClass( 'sorter-false' ) || nextSort === false ) { txt += ts.language.sortDisabled; } else { tmp = ( vars.count + 1 ) % vars.order.length; nextSort = vars.order[ tmp ]; // if nextSort txt += ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ]; } $header.attr( 'aria-label', txt ); if (vars.sortedBy) { $header.attr( 'data-sortedBy', vars.sortedBy ); } else { $header.removeAttr('data-sortedBy'); } } }, updateHeader : function( c ) { var index, isDisabled, $header, col, table = c.table, len = c.$headers.length; for ( index = 0; index < len; index++ ) { $header = c.$headers.eq( index ); col = ts.getColumnData( table, c.headers, index, true ); // add 'sorter-false' class if 'parser-false' is set isDisabled = ts.getData( $header, col, 'sorter' ) === 'false' || ts.getData( $header, col, 'parser' ) === 'false'; ts.setColumnSort( c, $header, isDisabled ); } }, setColumnSort : function( c, $header, isDisabled ) { var id = c.table.id; $header[ 0 ].sortDisabled = isDisabled; $header[ isDisabled ? 'addClass' : 'removeClass' ]( 'sorter-false' ) .attr( 'aria-disabled', '' + isDisabled ); // disable tab index on disabled cells if ( c.tabIndex ) { if ( isDisabled ) { $header.removeAttr( 'tabindex' ); } else { $header.attr( 'tabindex', '0' ); } } // aria-controls - requires table ID if ( id ) { if ( isDisabled ) { $header.removeAttr( 'aria-controls' ); } else { $header.attr( 'aria-controls', id ); } } }, updateHeaderSortCount : function( c, list ) { var col, dir, group, indx, primary, temp, val, order, sortList = list || c.sortList, len = sortList.length; c.sortList = []; for ( indx = 0; indx < len; indx++ ) { val = sortList[ indx ]; // ensure all sortList values are numeric - fixes #127 col = parseInt( val[ 0 ], 10 ); // prevents error if sorton array is wrong if ( col < c.columns ) { // set order if not already defined - due to colspan header without associated header cell // adding this check prevents a javascript error if ( !c.sortVars[ col ].order ) { if ( ts.getOrder( c.sortInitialOrder ) ) { order = c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ]; } else { order = c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ]; } c.sortVars[ col ].order = order; c.sortVars[ col ].count = 0; } order = c.sortVars[ col ].order; dir = ( '' + val[ 1 ] ).match( /^(1|d|s|o|n)/ ); dir = dir ? dir[ 0 ] : ''; // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext switch ( dir ) { case '1' : case 'd' : // descending dir = 1; break; case 's' : // same direction (as primary column) // if primary sort is set to 's', make it ascending dir = primary || 0; break; case 'o' : temp = order[ ( primary || 0 ) % order.length ]; // opposite of primary column; but resets if primary resets dir = temp === 0 ? 1 : temp === 1 ? 0 : 2; break; case 'n' : dir = order[ ( ++c.sortVars[ col ].count ) % order.length ]; break; default : // ascending dir = 0; break; } primary = indx === 0 ? dir : primary; group = [ col, parseInt( dir, 10 ) || 0 ]; c.sortList[ c.sortList.length ] = group; dir = $.inArray( group[ 1 ], order ); // fixes issue #167 c.sortVars[ col ].count = dir >= 0 ? dir : group[ 1 ] % order.length; } } }, updateAll : function( c, resort, callback ) { var table = c.table; table.isUpdating = true; ts.refreshWidgets( table, true, true ); ts.buildHeaders( c ); ts.bindEvents( table, c.$headers, true ); ts.bindMethods( c ); ts.commonUpdate( c, resort, callback ); }, update : function( c, resort, callback ) { var table = c.table; table.isUpdating = true; // update sorting (if enabled/disabled) ts.updateHeader( c ); ts.commonUpdate( c, resort, callback ); }, // simple header update - see #989 updateHeaders : function( c, callback ) { c.table.isUpdating = true; ts.buildHeaders( c ); ts.bindEvents( c.table, c.$headers, true ); ts.resortComplete( c, callback ); }, updateCell : function( c, cell, resort, callback ) { // updateCell for child rows is a mess - we'll ignore them for now // eventually I'll break out the "update" row cache code to make everything consistent if ( $( cell ).closest( 'tr' ).hasClass( c.cssChildRow ) ) { console.warn('Tablesorter Warning! "updateCell" for child row content has been disabled, use "update" instead'); return; } if ( ts.isEmptyObject( c.cache ) ) { // empty table, do an update instead - fixes #1099 ts.updateHeader( c ); ts.commonUpdate( c, resort, callback ); return; } c.table.isUpdating = true; c.$table.find( c.selectorRemove ).remove(); // get position from the dom var tmp, indx, row, icell, cache, len, $tbodies = c.$tbodies, $cell = $( cell ), // update cache - format: function( s, table, cell, cellIndex ) // no closest in jQuery v1.2.6 tbodyIndex = $tbodies.index( ts.getClosest( $cell, 'tbody' ) ), tbcache = c.cache[ tbodyIndex ], $row = ts.getClosest( $cell, 'tr' ); cell = $cell[ 0 ]; // in case cell is a jQuery object // tbody may not exist if update is initialized while tbody is removed for processing if ( $tbodies.length && tbodyIndex >= 0 ) { row = $tbodies.eq( tbodyIndex ).find( 'tr' ).not( '.' + c.cssChildRow ).index( $row ); cache = tbcache.normalized[ row ]; len = $row[ 0 ].cells.length; if ( len !== c.columns ) { // colspan in here somewhere! icell = 0; tmp = false; for ( indx = 0; indx < len; indx++ ) { if ( !tmp && $row[ 0 ].cells[ indx ] !== cell ) { icell += $row[ 0 ].cells[ indx ].colSpan; } else { tmp = true; } } } else { icell = $cell.index(); } tmp = ts.getElementText( c, cell, icell ); // raw cache[ c.columns ].raw[ icell ] = tmp; tmp = ts.getParsedText( c, cell, icell, tmp ); cache[ icell ] = tmp; // parsed if ( ( c.parsers[ icell ].type || '' ).toLowerCase() === 'numeric' ) { // update column max value (ignore sign) tbcache.colMax[ icell ] = Math.max( Math.abs( tmp ) || 0, tbcache.colMax[ icell ] || 0 ); } tmp = resort !== 'undefined' ? resort : c.resort; if ( tmp !== false ) { // widgets will be reapplied ts.checkResort( c, tmp, callback ); } else { // don't reapply widgets is resort is false, just in case it causes // problems with element focus ts.resortComplete( c, callback ); } } else { if ( ts.debug(c, 'core') ) { console.error( 'updateCell aborted, tbody missing or not within the indicated table' ); } c.table.isUpdating = false; } }, addRows : function( c, $row, resort, callback ) { var txt, val, tbodyIndex, rowIndex, rows, cellIndex, len, order, cacheIndex, rowData, cells, cell, span, // allow passing a row string if only one non-info tbody exists in the table valid = typeof $row === 'string' && c.$tbodies.length === 1 && / 0 ) { cacheIndex += span; } cacheIndex++; } // add the row data to the end cells[ c.columns ] = rowData; // update cache c.cache[ tbodyIndex ].normalized[ order ] = cells; } // resort using current settings ts.checkResort( c, resort, callback ); } }, updateCache : function( c, callback, $tbodies ) { // rebuild parsers if ( !( c.parsers && c.parsers.length ) ) { ts.setupParsers( c, $tbodies ); } // rebuild the cache map ts.buildCache( c, callback, $tbodies ); }, // init flag (true) used by pager plugin to prevent widget application // renamed from appendToTable appendCache : function( c, init ) { var parsed, totalRows, $tbody, $curTbody, rowIndex, tbodyIndex, appendTime, table = c.table, $tbodies = c.$tbodies, rows = [], cache = c.cache; // empty table - fixes #206/#346 if ( ts.isEmptyObject( cache ) ) { // run pager appender in case the table was just emptied return c.appender ? c.appender( table, rows ) : table.isUpdating ? c.$table.triggerHandler( 'updateComplete', table ) : ''; // Fixes #532 } if ( ts.debug(c, 'core') ) { appendTime = new Date(); } for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { $tbody = $tbodies.eq( tbodyIndex ); if ( $tbody.length ) { // detach tbody for manipulation $curTbody = ts.processTbody( table, $tbody, true ); parsed = cache[ tbodyIndex ].normalized; totalRows = parsed.length; for ( rowIndex = 0; rowIndex < totalRows; rowIndex++ ) { rows[rows.length] = parsed[ rowIndex ][ c.columns ].$row; // removeRows used by the pager plugin; don't render if using ajax - fixes #411 if ( !c.appender || ( c.pager && !c.pager.removeRows && !c.pager.ajax ) ) { $curTbody.append( parsed[ rowIndex ][ c.columns ].$row ); } } // restore tbody ts.processTbody( table, $curTbody, false ); } } if ( c.appender ) { c.appender( table, rows ); } if ( ts.debug(c, 'core') ) { console.log( 'Rebuilt table' + ts.benchmark( appendTime ) ); } // apply table widgets; but not before ajax completes if ( !init && !c.appender ) { ts.applyWidget( table ); } if ( table.isUpdating ) { c.$table.triggerHandler( 'updateComplete', table ); } }, commonUpdate : function( c, resort, callback ) { // remove rows/elements before update c.$table.find( c.selectorRemove ).remove(); // rebuild parsers ts.setupParsers( c ); // rebuild the cache map ts.buildCache( c ); ts.checkResort( c, resort, callback ); }, /* ▄█████ ▄████▄ █████▄ ██████ ██ █████▄ ▄████▄ ▀█▄ ██ ██ ██▄▄██ ██ ██ ██ ██ ██ ▄▄▄ ▀█▄ ██ ██ ██▀██ ██ ██ ██ ██ ██ ▀██ █████▀ ▀████▀ ██ ██ ██ ██ ██ ██ ▀████▀ */ initSort : function( c, cell, event ) { if ( c.table.isUpdating ) { // let any updates complete before initializing a sort return setTimeout( function() { ts.initSort( c, cell, event ); }, 50 ); } var arry, indx, headerIndx, dir, temp, tmp, $header, notMultiSort = !event[ c.sortMultiSortKey ], table = c.table, len = c.$headers.length, th = ts.getClosest( $( cell ), 'th, td' ), col = parseInt( th.attr( 'data-column' ), 10 ), sortedBy = event.type === 'mouseup' ? 'user' : event.type, order = c.sortVars[ col ].order; th = th[0]; // Only call sortStart if sorting is enabled c.$table.triggerHandler( 'sortStart', table ); // get current column sort order tmp = ( c.sortVars[ col ].count + 1 ) % order.length; c.sortVars[ col ].count = event[ c.sortResetKey ] ? 2 : tmp; // reset all sorts on non-current column - issue #30 if ( c.sortRestart ) { for ( headerIndx = 0; headerIndx < len; headerIndx++ ) { $header = c.$headers.eq( headerIndx ); tmp = parseInt( $header.attr( 'data-column' ), 10 ); // only reset counts on columns that weren't just clicked on and if not included in a multisort if ( col !== tmp && ( notMultiSort || $header.hasClass( ts.css.sortNone ) ) ) { c.sortVars[ tmp ].count = -1; } } } // user only wants to sort on one column if ( notMultiSort ) { $.each( c.sortVars, function( i ) { c.sortVars[ i ].sortedBy = ''; }); // flush the sort list c.sortList = []; c.last.sortList = []; if ( c.sortForce !== null ) { arry = c.sortForce; for ( indx = 0; indx < arry.length; indx++ ) { if ( arry[ indx ][ 0 ] !== col ) { c.sortList[ c.sortList.length ] = arry[ indx ]; c.sortVars[ arry[ indx ][ 0 ] ].sortedBy = 'sortForce'; } } } // add column to sort list dir = order[ c.sortVars[ col ].count ]; if ( dir < 2 ) { c.sortList[ c.sortList.length ] = [ col, dir ]; c.sortVars[ col ].sortedBy = sortedBy; // add other columns if header spans across multiple if ( th.colSpan > 1 ) { for ( indx = 1; indx < th.colSpan; indx++ ) { c.sortList[ c.sortList.length ] = [ col + indx, dir ]; // update count on columns in colSpan c.sortVars[ col + indx ].count = $.inArray( dir, order ); c.sortVars[ col + indx ].sortedBy = sortedBy; } } } // multi column sorting } else { // get rid of the sortAppend before adding more - fixes issue #115 & #523 c.sortList = $.extend( [], c.last.sortList ); // the user has clicked on an already sorted column if ( ts.isValueInArray( col, c.sortList ) >= 0 ) { // reverse the sorting direction c.sortVars[ col ].sortedBy = sortedBy; for ( indx = 0; indx < c.sortList.length; indx++ ) { tmp = c.sortList[ indx ]; if ( tmp[ 0 ] === col ) { // order.count seems to be incorrect when compared to cell.count tmp[ 1 ] = order[ c.sortVars[ col ].count ]; if ( tmp[1] === 2 ) { c.sortList.splice( indx, 1 ); c.sortVars[ col ].count = -1; } } } } else { // add column to sort list array dir = order[ c.sortVars[ col ].count ]; c.sortVars[ col ].sortedBy = sortedBy; if ( dir < 2 ) { c.sortList[ c.sortList.length ] = [ col, dir ]; // add other columns if header spans across multiple if ( th.colSpan > 1 ) { for ( indx = 1; indx < th.colSpan; indx++ ) { c.sortList[ c.sortList.length ] = [ col + indx, dir ]; // update count on columns in colSpan c.sortVars[ col + indx ].count = $.inArray( dir, order ); c.sortVars[ col + indx ].sortedBy = sortedBy; } } } } } // save sort before applying sortAppend c.last.sortList = $.extend( [], c.sortList ); if ( c.sortList.length && c.sortAppend ) { arry = $.isArray( c.sortAppend ) ? c.sortAppend : c.sortAppend[ c.sortList[ 0 ][ 0 ] ]; if ( !ts.isEmptyObject( arry ) ) { for ( indx = 0; indx < arry.length; indx++ ) { if ( arry[ indx ][ 0 ] !== col && ts.isValueInArray( arry[ indx ][ 0 ], c.sortList ) < 0 ) { dir = arry[ indx ][ 1 ]; temp = ( '' + dir ).match( /^(a|d|s|o|n)/ ); if ( temp ) { tmp = c.sortList[ 0 ][ 1 ]; switch ( temp[ 0 ] ) { case 'd' : dir = 1; break; case 's' : dir = tmp; break; case 'o' : dir = tmp === 0 ? 1 : 0; break; case 'n' : dir = ( tmp + 1 ) % order.length; break; default: dir = 0; break; } } c.sortList[ c.sortList.length ] = [ arry[ indx ][ 0 ], dir ]; c.sortVars[ arry[ indx ][ 0 ] ].sortedBy = 'sortAppend'; } } } } // sortBegin event triggered immediately before the sort c.$table.triggerHandler( 'sortBegin', table ); // setTimeout needed so the processing icon shows up setTimeout( function() { // set css for headers ts.setHeadersCss( c ); ts.multisort( c ); ts.appendCache( c ); c.$table.triggerHandler( 'sortBeforeEnd', table ); c.$table.triggerHandler( 'sortEnd', table ); }, 1 ); }, // sort multiple columns multisort : function( c ) { /*jshint loopfunc:true */ var tbodyIndex, sortTime, colMax, rows, tmp, table = c.table, sorter = [], dir = 0, textSorter = c.textSorter || '', sortList = c.sortList, sortLen = sortList.length, len = c.$tbodies.length; if ( c.serverSideSorting || ts.isEmptyObject( c.cache ) ) { // empty table - fixes #206/#346 return; } if ( ts.debug(c, 'core') ) { sortTime = new Date(); } // cache textSorter to optimize speed if ( typeof textSorter === 'object' ) { colMax = c.columns; while ( colMax-- ) { tmp = ts.getColumnData( table, textSorter, colMax ); if ( typeof tmp === 'function' ) { sorter[ colMax ] = tmp; } } } for ( tbodyIndex = 0; tbodyIndex < len; tbodyIndex++ ) { colMax = c.cache[ tbodyIndex ].colMax; rows = c.cache[ tbodyIndex ].normalized; rows.sort( function( a, b ) { var sortIndex, num, col, order, sort, x, y; // rows is undefined here in IE, so don't use it! for ( sortIndex = 0; sortIndex < sortLen; sortIndex++ ) { col = sortList[ sortIndex ][ 0 ]; order = sortList[ sortIndex ][ 1 ]; // sort direction, true = asc, false = desc dir = order === 0; if ( c.sortStable && a[ col ] === b[ col ] && sortLen === 1 ) { return a[ c.columns ].order - b[ c.columns ].order; } // fallback to natural sort since it is more robust num = /n/i.test( ts.getSortType( c.parsers, col ) ); if ( num && c.strings[ col ] ) { // sort strings in numerical columns if ( typeof ( ts.string[ c.strings[ col ] ] ) === 'boolean' ) { num = ( dir ? 1 : -1 ) * ( ts.string[ c.strings[ col ] ] ? -1 : 1 ); } else { num = ( c.strings[ col ] ) ? ts.string[ c.strings[ col ] ] || 0 : 0; } // fall back to built-in numeric sort // var sort = $.tablesorter['sort' + s]( a[col], b[col], dir, colMax[col], table ); sort = c.numberSorter ? c.numberSorter( a[ col ], b[ col ], dir, colMax[ col ], table ) : ts[ 'sortNumeric' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], num, colMax[ col ], col, c ); } else { // set a & b depending on sort direction x = dir ? a : b; y = dir ? b : a; // text sort function if ( typeof textSorter === 'function' ) { // custom OVERALL text sorter sort = textSorter( x[ col ], y[ col ], dir, col, table ); } else if ( typeof sorter[ col ] === 'function' ) { // custom text sorter for a SPECIFIC COLUMN sort = sorter[ col ]( x[ col ], y[ col ], dir, col, table ); } else { // fall back to natural sort sort = ts[ 'sortNatural' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ] || '', b[ col ] || '', col, c ); } } if ( sort ) { return sort; } } return a[ c.columns ].order - b[ c.columns ].order; }); } if ( ts.debug(c, 'core') ) { console.log( 'Applying sort ' + sortList.toString() + ts.benchmark( sortTime ) ); } }, resortComplete : function( c, callback ) { if ( c.table.isUpdating ) { c.$table.triggerHandler( 'updateComplete', c.table ); } if ( $.isFunction( callback ) ) { callback( c.table ); } }, checkResort : function( c, resort, callback ) { var sortList = $.isArray( resort ) ? resort : c.sortList, // if no resort parameter is passed, fallback to config.resort (true by default) resrt = typeof resort === 'undefined' ? c.resort : resort; // don't try to resort if the table is still processing // this will catch spamming of the updateCell method if ( resrt !== false && !c.serverSideSorting && !c.table.isProcessing ) { if ( sortList.length ) { ts.sortOn( c, sortList, function() { ts.resortComplete( c, callback ); }, true ); } else { ts.sortReset( c, function() { ts.resortComplete( c, callback ); ts.applyWidget( c.table, false ); } ); } } else { ts.resortComplete( c, callback ); ts.applyWidget( c.table, false ); } }, sortOn : function( c, list, callback, init ) { var indx, table = c.table; c.$table.triggerHandler( 'sortStart', table ); for (indx = 0; indx < c.columns; indx++) { c.sortVars[ indx ].sortedBy = ts.isValueInArray( indx, list ) > -1 ? 'sorton' : ''; } // update header count index ts.updateHeaderSortCount( c, list ); // set css for headers ts.setHeadersCss( c ); // fixes #346 if ( c.delayInit && ts.isEmptyObject( c.cache ) ) { ts.buildCache( c ); } c.$table.triggerHandler( 'sortBegin', table ); // sort the table and append it to the dom ts.multisort( c ); ts.appendCache( c, init ); c.$table.triggerHandler( 'sortBeforeEnd', table ); c.$table.triggerHandler( 'sortEnd', table ); ts.applyWidget( table ); if ( $.isFunction( callback ) ) { callback( table ); } }, sortReset : function( c, callback ) { c.sortList = []; var indx; for (indx = 0; indx < c.columns; indx++) { c.sortVars[ indx ].count = -1; c.sortVars[ indx ].sortedBy = ''; } ts.setHeadersCss( c ); ts.multisort( c ); ts.appendCache( c ); if ( $.isFunction( callback ) ) { callback( c.table ); } }, getSortType : function( parsers, column ) { return ( parsers && parsers[ column ] ) ? parsers[ column ].type || '' : ''; }, getOrder : function( val ) { // look for 'd' in 'desc' order; return true return ( /^d/i.test( val ) || val === 1 ); }, // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed) sortNatural : function( a, b ) { if ( a === b ) { return 0; } a = ( a || '' ).toString(); b = ( b || '' ).toString(); var aNum, bNum, aFloat, bFloat, indx, max, regex = ts.regex; // first try and sort Hex codes if ( regex.hex.test( b ) ) { aNum = parseInt( a.match( regex.hex ), 16 ); bNum = parseInt( b.match( regex.hex ), 16 ); if ( aNum < bNum ) { return -1; } if ( aNum > bNum ) { return 1; } } // chunk/tokenize aNum = a.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' ); bNum = b.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' ); max = Math.max( aNum.length, bNum.length ); // natural sorting through split numeric strings and default strings for ( indx = 0; indx < max; indx++ ) { // find floats not starting with '0', string or 0 if not defined aFloat = isNaN( aNum[ indx ] ) ? aNum[ indx ] || 0 : parseFloat( aNum[ indx ] ) || 0; bFloat = isNaN( bNum[ indx ] ) ? bNum[ indx ] || 0 : parseFloat( bNum[ indx ] ) || 0; // handle numeric vs string comparison - number < string - (Kyle Adams) if ( isNaN( aFloat ) !== isNaN( bFloat ) ) { return isNaN( aFloat ) ? 1 : -1; } // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' if ( typeof aFloat !== typeof bFloat ) { aFloat += ''; bFloat += ''; } if ( aFloat < bFloat ) { return -1; } if ( aFloat > bFloat ) { return 1; } } return 0; }, sortNaturalAsc : function( a, b, col, c ) { if ( a === b ) { return 0; } var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; } if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; } return ts.sortNatural( a, b ); }, sortNaturalDesc : function( a, b, col, c ) { if ( a === b ) { return 0; } var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; } if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; } return ts.sortNatural( b, a ); }, // basic alphabetical sort sortText : function( a, b ) { return a > b ? 1 : ( a < b ? -1 : 0 ); }, // return text string value by adding up ascii value // so the text is somewhat sorted when using a digital sort // this is NOT an alphanumeric sort getTextValue : function( val, num, max ) { if ( max ) { // make sure the text value is greater than the max numerical value (max) var indx, len = val ? val.length : 0, n = max + num; for ( indx = 0; indx < len; indx++ ) { n += val.charCodeAt( indx ); } return num * n; } return 0; }, sortNumericAsc : function( a, b, num, max, col, c ) { if ( a === b ) { return 0; } var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; } if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; } if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); } if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); } return a - b; }, sortNumericDesc : function( a, b, num, max, col, c ) { if ( a === b ) { return 0; } var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; } if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; } if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); } if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); } return b - a; }, sortNumeric : function( a, b ) { return a - b; }, /* ██ ██ ██ ██ █████▄ ▄████▄ ██████ ██████ ▄█████ ██ ██ ██ ██ ██ ██ ██ ▄▄▄ ██▄▄ ██ ▀█▄ ██ ██ ██ ██ ██ ██ ██ ▀██ ██▀▀ ██ ▀█▄ ███████▀ ██ █████▀ ▀████▀ ██████ ██ █████▀ */ addWidget : function( widget ) { if ( widget.id && !ts.isEmptyObject( ts.getWidgetById( widget.id ) ) ) { console.warn( '"' + widget.id + '" widget was loaded more than once!' ); } ts.widgets[ ts.widgets.length ] = widget; }, hasWidget : function( $table, name ) { $table = $( $table ); return $table.length && $table[ 0 ].config && $table[ 0 ].config.widgetInit[ name ] || false; }, getWidgetById : function( name ) { var indx, widget, len = ts.widgets.length; for ( indx = 0; indx < len; indx++ ) { widget = ts.widgets[ indx ]; if ( widget && widget.id && widget.id.toLowerCase() === name.toLowerCase() ) { return widget; } } }, applyWidgetOptions : function( table ) { var indx, widget, wo, c = table.config, len = c.widgets.length; if ( len ) { for ( indx = 0; indx < len; indx++ ) { widget = ts.getWidgetById( c.widgets[ indx ] ); if ( widget && widget.options ) { wo = $.extend( true, {}, widget.options ); c.widgetOptions = $.extend( true, wo, c.widgetOptions ); // add widgetOptions to defaults for option validator $.extend( true, ts.defaults.widgetOptions, widget.options ); } } } }, addWidgetFromClass : function( table ) { var len, indx, c = table.config, // look for widgets to apply from table class // don't match from 'ui-widget-content'; use \S instead of \w to include widgets // with dashes in the name, e.g. "widget-test-2" extracts out "test-2" regex = '^' + c.widgetClass.replace( ts.regex.templateName, '(\\S+)+' ) + '$', widgetClass = new RegExp( regex, 'g' ), // split up table class (widget id's can include dashes) - stop using match // otherwise only one widget gets extracted, see #1109 widgets = ( table.className || '' ).split( ts.regex.spaces ); if ( widgets.length ) { len = widgets.length; for ( indx = 0; indx < len; indx++ ) { if ( widgets[ indx ].match( widgetClass ) ) { c.widgets[ c.widgets.length ] = widgets[ indx ].replace( widgetClass, '$1' ); } } } }, applyWidgetId : function( table, id, init ) { table = $(table)[0]; var applied, time, name, c = table.config, wo = c.widgetOptions, debug = ts.debug(c, 'core'), widget = ts.getWidgetById( id ); if ( widget ) { name = widget.id; applied = false; // add widget name to option list so it gets reapplied after sorting, filtering, etc if ( $.inArray( name, c.widgets ) < 0 ) { c.widgets[ c.widgets.length ] = name; } if ( debug ) { time = new Date(); } if ( init || !( c.widgetInit[ name ] ) ) { // set init flag first to prevent calling init more than once (e.g. pager) c.widgetInit[ name ] = true; if ( table.hasInitialized ) { // don't reapply widget options on tablesorter init ts.applyWidgetOptions( table ); } if ( typeof widget.init === 'function' ) { applied = true; if ( debug ) { console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' ); } widget.init( table, widget, c, wo ); } } if ( !init && typeof widget.format === 'function' ) { applied = true; if ( debug ) { console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' ); } widget.format( table, c, wo, false ); } if ( debug ) { if ( applied ) { console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time ) ); if ( console.groupEnd ) { console.groupEnd(); } } } } }, applyWidget : function( table, init, callback ) { table = $( table )[ 0 ]; // in case this is called externally var indx, len, names, widget, time, c = table.config, debug = ts.debug(c, 'core'), widgets = []; // prevent numerous consecutive widget applications if ( init !== false && table.hasInitialized && ( table.isApplyingWidgets || table.isUpdating ) ) { return; } if ( debug ) { time = new Date(); } ts.addWidgetFromClass( table ); // prevent "tablesorter-ready" from firing multiple times in a row clearTimeout( c.timerReady ); if ( c.widgets.length ) { table.isApplyingWidgets = true; // ensure unique widget ids c.widgets = $.grep( c.widgets, function( val, index ) { return $.inArray( val, c.widgets ) === index; }); names = c.widgets || []; len = names.length; // build widget array & add priority as needed for ( indx = 0; indx < len; indx++ ) { widget = ts.getWidgetById( names[ indx ] ); if ( widget && widget.id ) { // set priority to 10 if not defined if ( !widget.priority ) { widget.priority = 10; } widgets[ indx ] = widget; } else if ( debug ) { console.warn( '"' + names[ indx ] + '" was enabled, but the widget code has not been loaded!' ); } } // sort widgets by priority widgets.sort( function( a, b ) { return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1; }); // add/update selected widgets len = widgets.length; if ( debug ) { console[ console.group ? 'group' : 'log' ]( 'Start ' + ( init ? 'initializing' : 'applying' ) + ' widgets' ); } for ( indx = 0; indx < len; indx++ ) { widget = widgets[ indx ]; if ( widget && widget.id ) { ts.applyWidgetId( table, widget.id, init ); } } if ( debug && console.groupEnd ) { console.groupEnd(); } } c.timerReady = setTimeout( function() { table.isApplyingWidgets = false; $.data( table, 'lastWidgetApplication', new Date() ); c.$table.triggerHandler( 'tablesorter-ready' ); // callback executed on init only if ( !init && typeof callback === 'function' ) { callback( table ); } if ( debug ) { widget = c.widgets.length; console.log( 'Completed ' + ( init === true ? 'initializing ' : 'applying ' ) + widget + ' widget' + ( widget !== 1 ? 's' : '' ) + ts.benchmark( time ) ); } }, 10 ); }, removeWidget : function( table, name, refreshing ) { table = $( table )[ 0 ]; var index, widget, indx, len, c = table.config; // if name === true, add all widgets from $.tablesorter.widgets if ( name === true ) { name = []; len = ts.widgets.length; for ( indx = 0; indx < len; indx++ ) { widget = ts.widgets[ indx ]; if ( widget && widget.id ) { name[ name.length ] = widget.id; } } } else { // name can be either an array of widgets names, // or a space/comma separated list of widget names name = ( $.isArray( name ) ? name.join( ',' ) : name || '' ).toLowerCase().split( /[\s,]+/ ); } len = name.length; for ( index = 0; index < len; index++ ) { widget = ts.getWidgetById( name[ index ] ); indx = $.inArray( name[ index ], c.widgets ); // don't remove the widget from config.widget if refreshing if ( indx >= 0 && refreshing !== true ) { c.widgets.splice( indx, 1 ); } if ( widget && widget.remove ) { if ( ts.debug(c, 'core') ) { console.log( ( refreshing ? 'Refreshing' : 'Removing' ) + ' "' + name[ index ] + '" widget' ); } widget.remove( table, c, c.widgetOptions, refreshing ); c.widgetInit[ name[ index ] ] = false; } } c.$table.triggerHandler( 'widgetRemoveEnd', table ); }, refreshWidgets : function( table, doAll, dontapply ) { table = $( table )[ 0 ]; // see issue #243 var indx, widget, c = table.config, curWidgets = c.widgets, widgets = ts.widgets, len = widgets.length, list = [], callback = function( table ) { $( table ).triggerHandler( 'refreshComplete' ); }; // remove widgets not defined in config.widgets, unless doAll is true for ( indx = 0; indx < len; indx++ ) { widget = widgets[ indx ]; if ( widget && widget.id && ( doAll || $.inArray( widget.id, curWidgets ) < 0 ) ) { list[ list.length ] = widget.id; } } ts.removeWidget( table, list.join( ',' ), true ); if ( dontapply !== true ) { // call widget init if ts.applyWidget( table, doAll || false, callback ); if ( doAll ) { // apply widget format ts.applyWidget( table, false, callback ); } } else { callback( table ); } }, /* ██ ██ ██████ ██ ██ ██ ██████ ██ ██████ ▄█████ ██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ▀█▄ ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀█▄ ▀████▀ ██ ██ ██████ ██ ██ ██ ██████ █████▀ */ benchmark : function( diff ) { return ( ' (' + ( new Date().getTime() - diff.getTime() ) + ' ms)' ); }, // deprecated ts.log log : function() { console.log( arguments ); }, debug : function(c, name) { return c && ( c.debug === true || typeof c.debug === 'string' && c.debug.indexOf(name) > -1 ); }, // $.isEmptyObject from jQuery v1.4 isEmptyObject : function( obj ) { /*jshint forin: false */ for ( var name in obj ) { return false; } return true; }, isValueInArray : function( column, arry ) { var indx, len = arry && arry.length || 0; for ( indx = 0; indx < len; indx++ ) { if ( arry[ indx ][ 0 ] === column ) { return indx; } } return -1; }, formatFloat : function( str, table ) { if ( typeof str !== 'string' || str === '' ) { return str; } // allow using formatFloat without a table; defaults to US number format var num, usFormat = table && table.config ? table.config.usNumberFormat !== false : typeof table !== 'undefined' ? table : true; if ( usFormat ) { // US Format - 1,234,567.89 -> 1234567.89 str = str.replace( ts.regex.comma, '' ); } else { // German Format = 1.234.567,89 -> 1234567.89 // French Format = 1 234 567,89 -> 1234567.89 str = str.replace( ts.regex.digitNonUS, '' ).replace( ts.regex.comma, '.' ); } if ( ts.regex.digitNegativeTest.test( str ) ) { // make (#) into a negative number -> (10) = -10 str = str.replace( ts.regex.digitNegativeReplace, '-$1' ); } num = parseFloat( str ); // return the text instead of zero return isNaN( num ) ? $.trim( str ) : num; }, isDigit : function( str ) { // replace all unwanted chars and match return isNaN( str ) ? ts.regex.digitTest.test( str.toString().replace( ts.regex.digitReplace, '' ) ) : str !== ''; }, // computeTableHeaderCellIndexes from: // http://www.javascripttoolbox.com/lib/table/examples.php // http://www.javascripttoolbox.com/temp/table_cellindex.html computeColumnIndex : function( $rows, c ) { var i, j, k, l, cell, cells, rowIndex, rowSpan, colSpan, firstAvailCol, // total columns has been calculated, use it to set the matrixrow columns = c && c.columns || 0, matrix = [], matrixrow = new Array( columns ); for ( i = 0; i < $rows.length; i++ ) { cells = $rows[ i ].cells; for ( j = 0; j < cells.length; j++ ) { cell = cells[ j ]; rowIndex = i; rowSpan = cell.rowSpan || 1; colSpan = cell.colSpan || 1; if ( typeof matrix[ rowIndex ] === 'undefined' ) { matrix[ rowIndex ] = []; } // Find first available column in the first row for ( k = 0; k < matrix[ rowIndex ].length + 1; k++ ) { if ( typeof matrix[ rowIndex ][ k ] === 'undefined' ) { firstAvailCol = k; break; } } // jscs:disable disallowEmptyBlocks if ( columns && cell.cellIndex === firstAvailCol ) { // don't to anything } else if ( cell.setAttribute ) { // jscs:enable disallowEmptyBlocks // add data-column (setAttribute = IE8+) cell.setAttribute( 'data-column', firstAvailCol ); } else { // remove once we drop support for IE7 - 1/12/2016 $( cell ).attr( 'data-column', firstAvailCol ); } for ( k = rowIndex; k < rowIndex + rowSpan; k++ ) { if ( typeof matrix[ k ] === 'undefined' ) { matrix[ k ] = []; } matrixrow = matrix[ k ]; for ( l = firstAvailCol; l < firstAvailCol + colSpan; l++ ) { matrixrow[ l ] = 'x'; } } } } ts.checkColumnCount($rows, matrix, matrixrow.length); return matrixrow.length; }, checkColumnCount : function($rows, matrix, columns) { // this DOES NOT report any tbody column issues, except for the math and // and column selector widgets var i, len, valid = true, cells = []; for ( i = 0; i < matrix.length; i++ ) { // some matrix entries are undefined when testing the footer because // it is using the rowIndex property if ( matrix[i] ) { len = matrix[i].length; if ( matrix[i].length !== columns ) { valid = false; break; } } } if ( !valid ) { $rows.each( function( indx, el ) { var cell = el.parentElement.nodeName; if ( cells.indexOf( cell ) < 0 ) { cells.push( cell ); } }); console.error( 'Invalid or incorrect number of columns in the ' + cells.join( ' or ' ) + '; expected ' + columns + ', but found ' + len + ' columns' ); } }, // automatically add a colgroup with col elements set to a percentage width fixColumnWidth : function( table ) { table = $( table )[ 0 ]; var overallWidth, percent, $tbodies, len, index, c = table.config, $colgroup = c.$table.children( 'colgroup' ); // remove plugin-added colgroup, in case we need to refresh the widths if ( $colgroup.length && $colgroup.hasClass( ts.css.colgroup ) ) { $colgroup.remove(); } if ( c.widthFixed && c.$table.children( 'colgroup' ).length === 0 ) { $colgroup = $( '' ); overallWidth = c.$table.width(); // only add col for visible columns - fixes #371 $tbodies = c.$tbodies.find( 'tr:first' ).children( ':visible' ); len = $tbodies.length; for ( index = 0; index < len; index++ ) { percent = parseInt( ( $tbodies.eq( index ).width() / overallWidth ) * 1000, 10 ) / 10 + '%'; $colgroup.append( $( '' ).css( 'width', percent ) ); } c.$table.prepend( $colgroup ); } }, // get sorter, string, empty, etc options for each column from // jQuery data, metadata, header option or header class name ('sorter-false') // priority = jQuery data > meta > headers option > header class name getData : function( header, configHeader, key ) { var meta, cl4ss, val = '', $header = $( header ); if ( !$header.length ) { return ''; } meta = $.metadata ? $header.metadata() : false; cl4ss = ' ' + ( $header.attr( 'class' ) || '' ); if ( typeof $header.data( key ) !== 'undefined' || typeof $header.data( key.toLowerCase() ) !== 'undefined' ) { // 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder' // 'data-sort-initial-order' is assigned to 'sortInitialOrder' val += $header.data( key ) || $header.data( key.toLowerCase() ); } else if ( meta && typeof meta[ key ] !== 'undefined' ) { val += meta[ key ]; } else if ( configHeader && typeof configHeader[ key ] !== 'undefined' ) { val += configHeader[ key ]; } else if ( cl4ss !== ' ' && cl4ss.match( ' ' + key + '-' ) ) { // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser' val = cl4ss.match( new RegExp( '\\s' + key + '-([\\w-]+)' ) )[ 1 ] || ''; } return $.trim( val ); }, getColumnData : function( table, obj, indx, getCell, $headers ) { if ( typeof obj !== 'object' || obj === null ) { return obj; } table = $( table )[ 0 ]; var $header, key, c = table.config, $cells = ( $headers || c.$headers ), // c.$headerIndexed is not defined initially $cell = c.$headerIndexed && c.$headerIndexed[ indx ] || $cells.find( '[data-column="' + indx + '"]:last' ); if ( typeof obj[ indx ] !== 'undefined' ) { return getCell ? obj[ indx ] : obj[ $cells.index( $cell ) ]; } for ( key in obj ) { if ( typeof key === 'string' ) { $header = $cell // header cell with class/id .filter( key ) // find elements within the header cell with cell/id .add( $cell.find( key ) ); if ( $header.length ) { return obj[ key ]; } } } return; }, // *** Process table *** // add processing indicator isProcessing : function( $table, toggle, $headers ) { $table = $( $table ); var c = $table[ 0 ].config, // default to all headers $header = $headers || $table.find( '.' + ts.css.header ); if ( toggle ) { // don't use sortList if custom $headers used if ( typeof $headers !== 'undefined' && c.sortList.length > 0 ) { // get headers from the sortList $header = $header.filter( function() { // get data-column from attr to keep compatibility with jQuery 1.2.6 return this.sortDisabled ? false : ts.isValueInArray( parseFloat( $( this ).attr( 'data-column' ) ), c.sortList ) >= 0; }); } $table.add( $header ).addClass( ts.css.processing + ' ' + c.cssProcessing ); } else { $table.add( $header ).removeClass( ts.css.processing + ' ' + c.cssProcessing ); } }, // detach tbody but save the position // don't use tbody because there are portions that look for a tbody index (updateCell) processTbody : function( table, $tb, getIt ) { table = $( table )[ 0 ]; if ( getIt ) { table.isProcessing = true; $tb.before( '' ); return $.fn.detach ? $tb.detach() : $tb.remove(); } var holdr = $( table ).find( 'colgroup.tablesorter-savemyplace' ); $tb.insertAfter( holdr ); holdr.remove(); table.isProcessing = false; }, clearTableBody : function( table ) { $( table )[ 0 ].config.$tbodies.children().detach(); }, // used when replacing accented characters during sorting characterEquivalents : { 'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå 'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ 'c' : '\u00e7\u0107\u010d', // çćč 'C' : '\u00c7\u0106\u010c', // ÇĆČ 'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę 'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ 'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı 'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ 'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöō 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ 'ss': '\u00df', // ß (s sharp) 'SS': '\u1e9e', // ẞ (Capital sharp s) 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ }, replaceAccents : function( str ) { var chr, acc = '[', eq = ts.characterEquivalents; if ( !ts.characterRegex ) { ts.characterRegexArray = {}; for ( chr in eq ) { if ( typeof chr === 'string' ) { acc += eq[ chr ]; ts.characterRegexArray[ chr ] = new RegExp( '[' + eq[ chr ] + ']', 'g' ); } } ts.characterRegex = new RegExp( acc + ']' ); } if ( ts.characterRegex.test( str ) ) { for ( chr in eq ) { if ( typeof chr === 'string' ) { str = str.replace( ts.characterRegexArray[ chr ], chr ); } } } return str; }, validateOptions : function( c ) { var setting, setting2, typ, timer, // ignore options containing an array ignore = 'headers sortForce sortList sortAppend widgets'.split( ' ' ), orig = c.originalSettings; if ( orig ) { if ( ts.debug(c, 'core') ) { timer = new Date(); } for ( setting in orig ) { typ = typeof ts.defaults[setting]; if ( typ === 'undefined' ) { console.warn( 'Tablesorter Warning! "table.config.' + setting + '" option not recognized' ); } else if ( typ === 'object' ) { for ( setting2 in orig[setting] ) { typ = ts.defaults[setting] && typeof ts.defaults[setting][setting2]; if ( $.inArray( setting, ignore ) < 0 && typ === 'undefined' ) { console.warn( 'Tablesorter Warning! "table.config.' + setting + '.' + setting2 + '" option not recognized' ); } } } } if ( ts.debug(c, 'core') ) { console.log( 'validate options time:' + ts.benchmark( timer ) ); } } }, // restore headers restoreHeaders : function( table ) { var index, $cell, c = $( table )[ 0 ].config, $headers = c.$table.find( c.selectorHeaders ), len = $headers.length; // don't use c.$headers here in case header cells were swapped for ( index = 0; index < len; index++ ) { $cell = $headers.eq( index ); // only restore header cells if it is wrapped // because this is also used by the updateAll method if ( $cell.find( '.' + ts.css.headerIn ).length ) { $cell.html( c.headerContent[ index ] ); } } }, destroy : function( table, removeClasses, callback ) { table = $( table )[ 0 ]; if ( !table.hasInitialized ) { return; } // remove all widgets ts.removeWidget( table, true, false ); var events, $t = $( table ), c = table.config, $h = $t.find( 'thead:first' ), $r = $h.find( 'tr.' + ts.css.headerRow ).removeClass( ts.css.headerRow + ' ' + c.cssHeaderRow ), $f = $t.find( 'tfoot:first > tr' ).children( 'th, td' ); if ( removeClasses === false && $.inArray( 'uitheme', c.widgets ) >= 0 ) { // reapply uitheme classes, in case we want to maintain appearance $t.triggerHandler( 'applyWidgetId', [ 'uitheme' ] ); $t.triggerHandler( 'applyWidgetId', [ 'zebra' ] ); } // remove widget added rows, just in case $h.find( 'tr' ).not( $r ).remove(); // disable tablesorter - not using .unbind( namespace ) because namespacing was // added in jQuery v1.4.3 - see http://api.jquery.com/event.namespace/ events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' + 'appendCache updateCache applyWidgetId applyWidgets refreshWidgets removeWidget destroy mouseup mouseleave ' + 'keypress sortBegin sortEnd resetToLoadState '.split( ' ' ) .join( c.namespace + ' ' ); $t .removeData( 'tablesorter' ) .unbind( events.replace( ts.regex.spaces, ' ' ) ); c.$headers .add( $f ) .removeClass( [ ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone ].join( ' ' ) ) .removeAttr( 'data-column' ) .removeAttr( 'aria-label' ) .attr( 'aria-disabled', 'true' ); $r .find( c.selectorSort ) .unbind( ( 'mousedown mouseup keypress '.split( ' ' ).join( c.namespace + ' ' ) ).replace( ts.regex.spaces, ' ' ) ); ts.restoreHeaders( table ); $t.toggleClass( ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false ); $t.removeClass(c.namespace.slice(1)); // clear flag in case the plugin is initialized again table.hasInitialized = false; delete table.config.cache; if ( typeof callback === 'function' ) { callback( table ); } if ( ts.debug(c, 'core') ) { console.log( 'tablesorter has been removed' ); } } }; $.fn.tablesorter = function( settings ) { return this.each( function() { var table = this, // merge & extend config options c = $.extend( true, {}, ts.defaults, settings, ts.instanceMethods ); // save initial settings c.originalSettings = settings; // create a table from data (build table widget) if ( !table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE' ) { // return the table (in case the original target is the table's container) ts.buildTable( table, c ); } else { ts.setup( table, c ); } }); }; // set up debug logs if ( !( window.console && window.console.log ) ) { // access $.tablesorter.logs for browsers that don't have a console... ts.logs = []; /*jshint -W020 */ console = {}; console.log = console.warn = console.error = console.table = function() { var arg = arguments.length > 1 ? arguments : arguments[0]; ts.logs[ ts.logs.length ] = { date: Date.now(), log: arg }; }; } // add default parsers ts.addParser({ id : 'no-parser', is : function() { return false; }, format : function() { return ''; }, type : 'text' }); ts.addParser({ id : 'text', is : function() { return true; }, format : function( str, table ) { var c = table.config; if ( str ) { str = $.trim( c.ignoreCase ? str.toLocaleLowerCase() : str ); str = c.sortLocaleCompare ? ts.replaceAccents( str ) : str; } return str; }, type : 'text' }); ts.regex.nondigit = /[^\w,. \-()]/g; ts.addParser({ id : 'digit', is : function( str ) { return ts.isDigit( str ); }, format : function( str, table ) { var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table ); return str && typeof num === 'number' ? num : str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str; }, type : 'numeric' }); ts.regex.currencyReplace = /[+\-,. ]/g; ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/; ts.addParser({ id : 'currency', is : function( str ) { str = ( str || '' ).replace( ts.regex.currencyReplace, '' ); // test for £$€¤¥¢ return ts.regex.currencyTest.test( str ); }, format : function( str, table ) { var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table ); return str && typeof num === 'number' ? num : str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str; }, type : 'numeric' }); // too many protocols to add them all https://en.wikipedia.org/wiki/URI_scheme // now, this regex can be updated before initialization ts.regex.urlProtocolTest = /^(https?|ftp|file):\/\//; ts.regex.urlProtocolReplace = /(https?|ftp|file):\/\/(www\.)?/; ts.addParser({ id : 'url', is : function( str ) { return ts.regex.urlProtocolTest.test( str ); }, format : function( str ) { return str ? $.trim( str.replace( ts.regex.urlProtocolReplace, '' ) ) : str; }, type : 'text' }); ts.regex.dash = /-/g; ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/; ts.addParser({ id : 'isoDate', is : function( str ) { return ts.regex.isoDate.test( str ); }, format : function( str ) { var date = str ? new Date( str.replace( ts.regex.dash, '/' ) ) : str; return date instanceof Date && isFinite( date ) ? date.getTime() : str; }, type : 'numeric' }); ts.regex.percent = /%/g; ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/; ts.addParser({ id : 'percent', is : function( str ) { return ts.regex.percentTest.test( str ) && str.length < 15; }, format : function( str, table ) { return str ? ts.formatFloat( str.replace( ts.regex.percent, '' ), table ) : str; }, type : 'numeric' }); // added image parser to core v2.17.9 ts.addParser({ id : 'image', is : function( str, table, node, $node ) { return $node.find( 'img' ).length > 0; }, format : function( str, table, cell ) { return $( cell ).find( 'img' ).attr( table.config.imgAttr || 'alt' ) || str; }, parsed : true, // filter widget flag type : 'text' }); ts.regex.dateReplace = /(\S)([AP]M)$/i; // used by usLongDate & time parser ts.regex.usLongDateTest1 = /^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i; ts.regex.usLongDateTest2 = /^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i; ts.addParser({ id : 'usLongDate', is : function( str ) { // two digit years are not allowed cross-browser // Jan 01, 2013 12:34:56 PM or 01 Jan 2013 return ts.regex.usLongDateTest1.test( str ) || ts.regex.usLongDateTest2.test( str ); }, format : function( str ) { var date = str ? new Date( str.replace( ts.regex.dateReplace, '$1 $2' ) ) : str; return date instanceof Date && isFinite( date ) ? date.getTime() : str; }, type : 'numeric' }); // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included ts.regex.shortDateTest = /(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/; // escaped "-" because JSHint in Firefox was showing it as an error ts.regex.shortDateReplace = /[\-.,]/g; // XXY covers MDY & DMY formats ts.regex.shortDateXXY = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/; ts.regex.shortDateYMD = /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/; ts.convertFormat = function( dateString, format ) { dateString = ( dateString || '' ) .replace( ts.regex.spaces, ' ' ) .replace( ts.regex.shortDateReplace, '/' ); if ( format === 'mmddyyyy' ) { dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$1/$2' ); } else if ( format === 'ddmmyyyy' ) { dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$2/$1' ); } else if ( format === 'yyyymmdd' ) { dateString = dateString.replace( ts.regex.shortDateYMD, '$1/$2/$3' ); } var date = new Date( dateString ); return date instanceof Date && isFinite( date ) ? date.getTime() : ''; }; ts.addParser({ id : 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd' is : function( str ) { str = ( str || '' ).replace( ts.regex.spaces, ' ' ).replace( ts.regex.shortDateReplace, '/' ); return ts.regex.shortDateTest.test( str ); }, format : function( str, table, cell, cellIndex ) { if ( str ) { var c = table.config, $header = c.$headerIndexed[ cellIndex ], format = $header.length && $header.data( 'dateFormat' ) || ts.getData( $header, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat' ) || c.dateFormat; // save format because getData can be slow... if ( $header.length ) { $header.data( 'dateFormat', format ); } return ts.convertFormat( str, format ) || str; } return str; }, type : 'numeric' }); // match 24 hour time & 12 hours time + am/pm - see http://regexr.com/3c3tk ts.regex.timeTest = /^(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)$|^((?:[01]\d|[2][0-4]):[0-5]\d)$/i; ts.regex.timeMatch = /(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i; ts.addParser({ id : 'time', is : function( str ) { return ts.regex.timeTest.test( str ); }, format : function( str ) { // isolate time... ignore month, day and year var temp, timePart = ( str || '' ).match( ts.regex.timeMatch ), orig = new Date( str ), // no time component? default to 00:00 by leaving it out, but only if str is defined time = str && ( timePart !== null ? timePart[ 0 ] : '00:00 AM' ), date = time ? new Date( '2000/01/01 ' + time.replace( ts.regex.dateReplace, '$1 $2' ) ) : time; if ( date instanceof Date && isFinite( date ) ) { temp = orig instanceof Date && isFinite( orig ) ? orig.getTime() : 0; // if original string was a valid date, add it to the decimal so the column sorts in some kind of order // luckily new Date() ignores the decimals return temp ? parseFloat( date.getTime() + '.' + orig.getTime() ) : date.getTime(); } return str; }, type : 'numeric' }); ts.addParser({ id : 'metadata', is : function() { return false; }, format : function( str, table, cell ) { var c = table.config, p = ( !c.parserMetadataName ) ? 'sortValue' : c.parserMetadataName; return $( cell ).metadata()[ p ]; }, type : 'numeric' }); /* ██████ ██████ █████▄ █████▄ ▄████▄ ▄█▀ ██▄▄ ██▄▄██ ██▄▄██ ██▄▄██ ▄█▀ ██▀▀ ██▀▀██ ██▀▀█ ██▀▀██ ██████ ██████ █████▀ ██ ██ ██ ██ */ // add default widgets ts.addWidget({ id : 'zebra', priority : 90, format : function( table, c, wo ) { var $visibleRows, $row, count, isEven, tbodyIndex, rowIndex, len, child = new RegExp( c.cssChildRow, 'i' ), $tbodies = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody:not(.' + c.cssInfoBlock + ')' ) ); for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { // loop through the visible rows count = 0; $visibleRows = $tbodies.eq( tbodyIndex ).children( 'tr:visible' ).not( c.selectorRemove ); len = $visibleRows.length; for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { $row = $visibleRows.eq( rowIndex ); // style child rows the same way the parent row was styled if ( !child.test( $row[ 0 ].className ) ) { count++; } isEven = ( count % 2 === 0 ); $row .removeClass( wo.zebra[ isEven ? 1 : 0 ] ) .addClass( wo.zebra[ isEven ? 0 : 1 ] ); } } }, remove : function( table, c, wo, refreshing ) { if ( refreshing ) { return; } var tbodyIndex, $tbody, $tbodies = c.$tbodies, toRemove = ( wo.zebra || [ 'even', 'odd' ] ).join( ' ' ); for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody $tbody.children().removeClass( toRemove ); ts.processTbody( table, $tbody, false ); // restore tbody } } }); })( jQuery ); ================================================ FILE: src/main/resources/static/assets/js/prism.js ================================================ /* PrismJS 1.19.0 https://prismjs.com/download.html#themes=prism-okaidia&languages=bash+yaml&plugins=toolbar+copy-to-clipboard */ var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,n=0,C={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof _?new _(e.type,C.util.encode(e.content),e.alias):Array.isArray(e)?e.map(C.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(k instanceof _)){if(h&&y!=n.length-1){if(c.lastIndex=v,!(O=c.exec(e)))break;for(var b=O.index+(f&&O[1]?O[1].length:0),w=O.index+O[0].length,A=y,P=v,x=n.length;A"+r.content+""},!u.document)return u.addEventListener&&(C.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,t=n.code,a=n.immediateClose;u.postMessage(C.highlight(t,C.languages[r],r)),a&&u.close()},!1)),C;var e=C.util.currentScript();if(e&&(C.filename=e.src,e.hasAttribute("data-manual")&&(C.manual=!0)),!C.manual){function r(){C.manual||C.highlightAll()}var t=document.readyState;"loading"===t||"interactive"===t&&e&&e.defer?document.addEventListener("DOMContentLoaded",r):window.requestAnimationFrame?window.requestAnimationFrame(r):window.setTimeout(r,16)}return C}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); !function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",n={environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--?|-=|\+\+?|\+=|!=?|~|\*\*?|\*=|\/=?|%=?|<<=?|>>=?|<=?|>=?|==?|&&?|&=|\^=?|\|\|?|\|=|\?|:/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)\w+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b\w+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+?)\s*(?:\r?\n|\r)(?:[\s\S])*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s*(?:\r?\n|\r)(?:[\s\S])*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0},{pattern:/(["'])(?:\\[\s\S]|\$\([^)]+\)|`[^`]+`|(?!\1)[^\\])*\1/,greedy:!0,inside:n}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|aptitude|apt-cache|apt-get|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:if|then|else|elif|fi|for|while|in|case|esac|function|select|do|done|until)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|break|cd|continue|eval|exec|exit|export|getopts|hash|pwd|readonly|return|shift|test|times|trap|umask|unset|alias|bind|builtin|caller|command|declare|echo|enable|help|let|local|logout|mapfile|printf|read|readarray|source|type|typeset|ulimit|unalias|set|shopt)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:true|false)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|==?|!=?|=~|<<[<-]?|[&\d]?>>|\d?[<>]&?|&[>&]?|\|[&|]?|<=?|>=?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}};for(var a=["comment","function-name","for-or-select","assign-left","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],r=n.variable[1].inside,s=0;s])[ \t]*(?:((?:\r?\n|\r)[ \t]+)[^\r\n]+(?:\2[^\r\n]+)*)/,lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:/(\s*(?:^|[:\-,[{\r\n?])[ \t]*(?:![^\s]+)?[ \t]*)[^\r\n{[\]},#\s]+?(?=\s*:\s)/,lookbehind:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)(?:\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?)?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?)(?=[ \t]*(?:$|,|]|}))/m,lookbehind:!0,alias:"number"},boolean:{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)(?:true|false)[ \t]*(?=$|,|]|})/im,lookbehind:!0,alias:"important"},null:{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)(?:null|~)[ \t]*(?=$|,|]|})/im,lookbehind:!0,alias:"important"},string:{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)("|')(?:(?!\2)[^\\\r\n]|\\.)*\2(?=[ \t]*(?:$|,|]|}|\s*#))/m,lookbehind:!0,greedy:!0},number:{pattern:/([:\-,[{]\s*(?:![^\s]+)?[ \t]*)[+-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+\.?\d*|\.?\d+)(?:e[+-]?\d+)?|\.inf|\.nan)[ \t]*(?=$|,|]|})/im,lookbehind:!0},tag:/![^\s]+/,important:/[&*][\w]+/,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},Prism.languages.yml=Prism.languages.yaml; !function(){if("undefined"!=typeof self&&self.Prism&&self.document){var r=[],i={},a=function(){};Prism.plugins.toolbar={};var t=Prism.plugins.toolbar.registerButton=function(t,a){var e;e="function"==typeof a?a:function(t){var e;return"function"==typeof a.onClick?((e=document.createElement("button")).type="button",e.addEventListener("click",function(){a.onClick.call(this,t)})):"string"==typeof a.url?(e=document.createElement("a")).href=a.url:e=document.createElement("span"),a.className&&e.classList.add(a.className),e.textContent=a.text,e},t in i?console.warn('There is a button with the key "'+t+'" registered already.'):r.push(i[t]=e)},e=Prism.plugins.toolbar.hook=function(n){var t=n.element.parentNode;if(t&&/pre/i.test(t.nodeName)&&!t.parentNode.classList.contains("code-toolbar")){var e=document.createElement("div");e.classList.add("code-toolbar"),t.parentNode.insertBefore(e,t),e.appendChild(t);var o=document.createElement("div");o.classList.add("toolbar"),document.body.hasAttribute("data-toolbar-order")&&(r=document.body.getAttribute("data-toolbar-order").split(",").map(function(t){return i[t]||a})),r.forEach(function(t){var e=t(n);if(e){var a=document.createElement("div");a.classList.add("toolbar-item"),a.appendChild(e),o.appendChild(a)}}),e.appendChild(o)}};t("label",function(t){var e=t.element.parentNode;if(e&&/pre/i.test(e.nodeName)&&e.hasAttribute("data-label")){var a,n,o=e.getAttribute("data-label");try{n=document.querySelector("template#"+o)}catch(t){}return n?a=n.content:(e.hasAttribute("data-url")?(a=document.createElement("a")).href=e.getAttribute("data-url"):a=document.createElement("span"),a.textContent=o),a}}),Prism.hooks.add("complete",e)}}(); !function(){if("undefined"!=typeof self&&self.Prism&&self.document)if(Prism.plugins.toolbar){var r=window.ClipboardJS||void 0;r||"function"!=typeof require||(r=require("clipboard"));var i=[];if(!r){var o=document.createElement("script"),e=document.querySelector("head");o.onload=function(){if(r=window.ClipboardJS)for(;i.length;)i.pop()()},o.src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js",e.appendChild(o)}Prism.plugins.toolbar.registerButton("copy-to-clipboard",function(e){var t=document.createElement("button");return t.textContent="Copy",r?o():i.push(o),t;function o(){var o=new r(t,{text:function(){return e.code}});o.on("success",function(){t.textContent="Copied!",n()}),o.on("error",function(){t.textContent="Press Ctrl+C to copy",n()})}function n(){setTimeout(function(){t.textContent="Copy"},5e3)}})}else console.warn("Copy to Clipboard plugin loaded before Toolbar plugin.")}(); ================================================ FILE: src/main/resources/version.properties ================================================ #Sun Oct 30 08:13:05 GMT 2022 app.build.date=2022-10-30T08\:13\:05 app.build.os=Mac OS X app.build.user=josh app.version=2.3.3 ================================================ FILE: src/main/resources/views/pages/admin/image-edit.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../prebuilt/base.ftl" as base /> <#import "../../prebuilt/fleet-title.ftl" as title /> <#import "../../ui/components/dropdown.ftl" as dropdown /> <#import "../../ui/layout/section.ftl" as section /> <#import "../../ui/layout/container.ftl" as container /> <#import "../../ui/form/input.ftl" as input /> <#import "../../ui/elements/button.ftl" as button /> <#import "../../ui/elements/table.ftl" as table /> <#import "../../ui/elements/tag.ftl" as tag /> <#import "../../ui/components/message.ftl" as message /> <#import "template-components/image-template-ports.ftl" as templatePorts /> <#import "template-components/image-template-volumes.ftl" as templateVolumes /> <#import "template-components/image-template-environment.ftl" as templateEnvironment /> <#import "template-components/image-template-devices.ftl" as templateDevices /> <#import "template-components/image-template-misc.ftl" as templateMisc /> <@base.base title='Edit ${image.name} | Admin' context="admin_image_edit"> <#if image?has_content> <@section.section id="ManageImage"> <@container.container>
<@title.title icon="cube" thinValue=image.repositoryName boldValue=image.name separator="/" subtitle="Update metadata and tracked branches" />
<#-- General base information which is to be added manually (data which can't necessarily be inferred from upstream) -->

General

<#if image.metaData.appImagePath?has_content>
${image.name} logo
<@input.text id="ImageBase" label="Base Image" isInline=true value=image.metaData.baseImage infoText="The name of the base image this image pulls from." />
<@input.text id="ImageCategory" label="Category" isInline=true value=image.metaData.category infoText="The application category for this image (e.g Home Automation)." />
<@button.buttons isRightAligned=true> <@button.submit colour="success" extraClasses="is-fullwidth-mobile"> Save General Changes

External Urls

<@table.table id="ImageExternalUrls"> Type Name Absolute Path <#list image.metaData.externalUrls as url>
<@button.buttons isRightAligned=true> <@button.button colour="danger" size="small" extraClasses="remove-image-external-url"> <@button.buttons isRightAligned=true> <@button.button id="AddNewExternalUrl" colour="normal-colour" size="small"> Add Url
<@button.buttons isRightAligned=true> <@button.submit colour="success" extraClasses="is-fullwidth-mobile"> Update External Urls
<#-- Tag branches -->

Tracked Tag Branches

<@message.message colour="info"> Recorded branches here will be used to look up latest linked versions. The latest tag is default and cannot be deleted.
<@table.table id="ImageTrackedBranches" isScrollable=true isFullWidth=true> Branch Name <#list image.tagBranches as tagBranch> ${tagBranch.branchName?html} <#if !tagBranch.branchProtected> <@button.buttons isRightAligned=true> <@button.button extraClasses="remove-tag-branch" colour="white" size="small" title="Stop tracking this branch."> <#else>
<@tag.tag colour="light" value="Protected" extraAttributes='title="This branch can\'t be removed."' />
<@input.text id="NewTrackedBranch" icon="sitemap" size="small" /> <@button.buttons isRightAligned=true> <@button.button id="TrackNewBranch" size="small" colour="success"> Track
<#-- Port/Volume mappings for containers created from this image -->

Container Template

<@message.message colour="info"> This information is for display purposes and has no impact on this image's synchronisation processes. The data stored here will be used to generate various templates for downstream systems to consume, as well as for the display page to provide run commands and Compose snippets
<@templatePorts.ports ports=image.metaData.templates.ports />
<@templateVolumes.volumes volumes=image.metaData.templates.volumes />
<@templateEnvironment.environment environment=image.metaData.templates.env />
<@templateDevices.devices devices=image.metaData.templates.devices />
<@templateMisc.misc templateHolder=image.metaData.templates />
<@button.buttons isRightAligned=true> <@button.submit id="SaveTemplateChanges" colour="success" extraClasses="is-fullwidth-mobile"> Save Template Changes
<#else> <@section.section id="ManageImages"> <@container.container> Unable to find repository. ================================================ FILE: src/main/resources/views/pages/admin/images.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../prebuilt/base.ftl" as base /> <#import "../../prebuilt/fleet-title.ftl" as title /> <#import "../../ui/components/dropdown.ftl" as dropdown /> <#import "../../ui/layout/section.ftl" as section /> <#import "../../ui/layout/container.ftl" as container /> <#import "../../ui/form/input.ftl" as input /> <#import "../../ui/elements/button.ftl" as button /> <#import "../../ui/elements/table.ftl" as table /> <@base.base title='Images (${(repository.name)!"Unknown Repository"}) | Admin' context="admin_images"> <#if repository?has_content> <@section.section id="ManageImages"> <@container.container>
<@title.title thinValue="Images under" separator=" " boldValue="${repository.name}" subtitle="Manage all synchronised images" />
<@input.text id="SearchImages" icon="search" placeholder="Search..." />
<@table.table id="ImageTable" isFullWidth=true isHoverable=true isScrollable=true extraClasses="table--sortable"> Name Version Mask Sync Stable Hidden Dep. <#list repository.images as image> ${image.name} <@input.switchable id="VersionMask_${repository.key.id}" icon="mask" value=image.versionMask size="small" acceptClass="update-image-trigger" /> <@input.toggle id="Enabled_${image.key.id}" size="small" inputClasses="update-image-trigger" isToggled=(repository.syncEnabled && image.syncEnabled) isDisabled=(!repository.syncEnabled) title="${(!repository.syncEnabled)?string('Repository sync has been disabled.', '')}" /> <@input.toggle id="Stable_${image.key.id}" size="small" inputClasses="update-image-trigger" isToggled=image.stable /> <@input.toggle id="Hidden_${image.key.id}" colour="warning" size="small" inputClasses="update-image-trigger" isToggled=image.hidden /> <@input.toggle id="Deprecated_${image.key.id}" colour="danger" size="small" inputClasses="update-image-trigger" isToggled=image.deprecated /> <@button.buttons isGrouped=true isRightAligned=true> <@button.button id="ForceResync_${image.key.id}" size="small" title="Force resync" colour="normal-colour" extraAttributes='data-image-key="${image.key}"' extraClasses="sync-image"> <@button.link size="small" title="Edit image metadata" colour="normal-colour" link="/admin/image?imageKey=${image.key}">
<#else> <@section.section id="ManageImages"> <@container.container> Unable to find repository. ================================================ FILE: src/main/resources/views/pages/admin/repositories.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../prebuilt/base.ftl" as base /> <#import "../../prebuilt/fleet-title.ftl" as title /> <#import "../../ui/components/message.ftl" as message /> <#import "../../ui/components/modal.ftl" as modal /> <#import "../../ui/layout/section.ftl" as section /> <#import "../../ui/layout/container.ftl" as container /> <#import "../../ui/form/input.ftl" as input /> <#import "../../ui/elements/button.ftl" as button /> <#import "../../ui/elements/table.ftl" as table /> <@base.base title="Repositories | Admin" context="admin_repositories"> <@section.section id="ManageRepositories"> <@container.container>
<@title.title boldValue="Repositories" icon="cubes" subtitle="Manage the sync status of all added repositories" />
<@table.table isFullWidth=true isHoverable=true isScrollable=true> Enabled Name Version Mask <#list repositories as repository> <@input.toggle id="Enabled_${repository.key.id}" size="large" isToggled=repository.syncEnabled inputClasses="update-repository-trigger" />

${repository.name}

${repository.images?size} images <@input.switchable id="VersionMask_${repository.key.id}" icon="mask" value=repository.versionMask size="small" acceptClass="update-repository-trigger" /> <@button.buttons isGrouped=true isRightAligned=true> <@button.button size="small" colour="normal-colour" extraClasses="sync-repository" extraAttributes='data-repository-key="${repository.key}"' title="Re-sync repository"> <@button.button size="small" colour="danger" extraClasses="delete-repository" modal="#ConfirmDelete" extraAttributes='data-repository-name="${repository.name}" data-repository-key="${repository.key}"' title="Delete repository"> <@input.text id="NewRepositoryName" icon="cubes" placeholder="New Repository..." /> <@button.button id="SubmitNewRepository" size="small" colour="success"> Add
<@modal.modal id="ConfirmDelete" title="Are you sure?" isDismissable=true extraClasses="has-text-centered"> You are about to delete from the cache. If you wish to synchronise this repository at a later date, you will need to re-add it.

This action will permanently delete all historic pull data for all images under this repository.

<@button.buttons isRightAligned=true> <@button.button colour="light" extraClasses="is-modal-cancel is-wide-mobile"> Cancel <@button.button id="DeleteRepository" colour="danger is-wide-mobile"> Delete ================================================ FILE: src/main/resources/views/pages/admin/schedules.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../prebuilt/base.ftl" as base /> <#import "../../prebuilt/fleet-title.ftl" as title /> <#import "../../ui/layout/section.ftl" as section /> <#import "../../ui/layout/container.ftl" as container /> <#import "../../ui/elements/button.ftl" as button /> <#import "../../ui/elements/table.ftl" as table /> <@base.base title="Schedules | Admin" context="admin_schedules"> <@section.section id="LoadedSchedules"> <@container.container>
<@title.title boldValue="Schedules" icon="clock" subtitle="All loaded scheduled tasks which run periodically" />
<@table.table isFullWidth=true isHoverable=true isScrollable=true> Name Last Run Next Run (Est.) Interval <#list schedules as schedule> ${schedule.name} <#if schedule.lastRunTime?has_content> ${formatDate(schedule.lastRunTime, 'dd MMM yyyy HH:mm:ss')} <#else> Never ${formatDate(schedule.nextRunTime, 'dd MMM yyyy HH:mm:ss')} ${schedule.interval.timeDuration} ${schedule.interval.timeUnit?lower_case} <@button.buttons isGrouped=true isRightAligned=true> <@button.button extraClasses="force-schedule-run" colour="normal-colour" size="small" title="Run this schedule now" extraAttributes='data-schedule-key="${schedule.key}"'>

Queued Items

The synchronisation queue contains individual sync requests for images.

There ${(queueSize==1)?string('is', 'are')} currently #{queueSize} ${(queueSize==1)?string('item', 'items')} queued.

Request Consumer

Asynchronous thread which subscribes to the request queue.

The request consumer is currently ${consumerRunning?string('running', 'not running')}.
================================================ FILE: src/main/resources/views/pages/admin/template-components/image-template-devices.ftl ================================================ <#-- Copyright (c) 2020 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../../ui/elements/table.ftl" as table /> <#import "../../../ui/form/input.ftl" as input /> <#import "../../../ui/elements/button.ftl" as button /> <#macro devices devices> <@table.table isScrollable=true isFullWidth=true> Device Description <#list devices as device> <@button.buttons isRightAligned=true> <@button.button colour="danger" size="small" extraClasses="remove-image-template-item"> <@button.buttons isRightAligned=true> <@button.button id="AddNewDevice" colour="normal-colour" size="small"> Add ================================================ FILE: src/main/resources/views/pages/admin/template-components/image-template-environment.ftl ================================================ <#-- Copyright (c) 2020 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../../ui/elements/table.ftl" as table /> <#import "../../../ui/form/input.ftl" as input /> <#import "../../../ui/elements/button.ftl" as button /> <#macro environment environment> <@table.table isScrollable=true isFullWidth=true> Environment Variable Example Value Description <#list environment as env> <@button.buttons isRightAligned=true> <@button.button colour="danger" size="small" extraClasses="remove-image-template-item"> <@button.buttons isRightAligned=true> <@button.button id="AddNewEnv" colour="normal-colour" size="small"> Add ================================================ FILE: src/main/resources/views/pages/admin/template-components/image-template-misc.ftl ================================================ <#-- Copyright (c) 2020 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../../ui/form/input.ftl" as input /> <#macro misc templateHolder restartPolicies=[ 'no', 'always', 'unless-stopped', 'on-failure' ]>
<@input.text id="ImageTemplateUpstreamUrl" label="Registry Url" value=templateHolder.registryUrl!"" />
<@input.dropdown label="Restart Policy" id="ImageTemplateRestartPolicy"> <#list restartPolicies as policy> selected value="${policy}">${policy}
<@input.toggle id="ImageTemplateNetworkHost" label="Host Network" size="large" isToggled=templateHolder.hostNetworkingEnabled />
<@input.toggle id="ImageTemplatePrivileged" label="Privileged" size="large" isToggled=templateHolder.privilegedMode />
<@input.dropdown id="ImageTemplateCapabilities" label="Capabilities" isMultiple=true> <#list containerCapabilities as capability> selected value="${capability}">${capability}
================================================ FILE: src/main/resources/views/pages/admin/template-components/image-template-ports.ftl ================================================ <#-- Copyright (c) 2020 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../../ui/elements/table.ftl" as table /> <#import "../../../ui/form/input.ftl" as input /> <#import "../../../ui/elements/button.ftl" as button /> <#macro ports ports protocols=[ 'tcp', 'udp' ]> <@table.table isScrollable=true isFullWidth=true> Port Protocol Description <#list ports as port>
<@button.buttons isRightAligned=true> <@button.button colour="danger" size="small" extraClasses="remove-image-template-item"> <@button.buttons isRightAligned=true> <@button.button id="AddNewPort" colour="normal-colour" size="small"> Add ================================================ FILE: src/main/resources/views/pages/admin/template-components/image-template-volumes.ftl ================================================ <#-- Copyright (c) 2020 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../../ui/elements/table.ftl" as table /> <#import "../../../ui/form/input.ftl" as input /> <#import "../../../ui/elements/button.ftl" as button /> <#macro volumes volumes> <@table.table isScrollable=true isFullWidth=true> Volume Read Only? Description <#list volumes as volume>
<@button.buttons isRightAligned=true> <@button.button colour="danger" size="small" extraClasses="remove-image-template-item"> <@button.buttons isRightAligned=true> <@button.button id="AddNewVolume" colour="normal-colour" size="small"> Add ================================================ FILE: src/main/resources/views/pages/admin/users.ftl ================================================ <#-- Copyright (c) 2020 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../../prebuilt/base.ftl" as base /> <#import "../../prebuilt/fleet-title.ftl" as title /> <#import "../../ui/components/modal.ftl" as modal /> <#import "../../ui/elements/button.ftl" as button /> <#import "../../ui/elements/table.ftl" as table /> <#import "../../ui/form/input.ftl" as input /> <#import "../../ui/layout/section.ftl" as section /> <#import "../../ui/layout/container.ftl" as container /> <@base.base title='Users | Admin' context="admin_users"> <@section.section id="LoadedSchedules"> <@container.container>
<@title.title boldValue="Users" icon="users" subtitle="Add/remove or update system users" />
<@table.table isFullWidth=true isHoverable=true isScrollable=true> Username Role <#list users as user> ${user.username} ${user.role} <@button.buttons isGrouped=true isRightAligned=true> <@button.button size="small" colour="normal-colour" extraClasses="edit-password" modal="#EditUserPassword" title="Change password"> <#if users?size > 1 && user.username!=__AuthenticatedUser.name> <@button.button size="small" colour="danger" extraClasses="delete-user" modal="#ConfirmDeleteUser" title="Delete user">

New User

<@input.text id="NewUserName" size="small" label="Username" icon="user" isInline=true isRequired=true /> <@input.password id="NewUserPassword" size="small" label="Password" icon="lock" isInline=true isRequired=true /> <@button.buttons isRightAligned=true> <@button.submit id="CreateUser" colour="primary is-wide-mobile"> Create new user
<@modal.modal id="ConfirmDeleteUser" title="Are you sure?" isDismissable=true extraClasses="has-text-centered"> You are about to delete from this app.

This action will permanently delete this user.

<@button.buttons isRightAligned=true> <@button.button colour="light" extraClasses="is-modal-cancel is-wide-mobile"> Cancel <@button.submit id="DeleteUser" colour="danger is-wide-mobile"> Delete
<@modal.modal id="EditUserPassword" title="Update Password" isDismissable=true extraClasses="has-text-centered"> Update the password for .
<@input.password id="UserPassword" /> <@button.buttons isRightAligned=true> <@button.button colour="light" extraClasses="is-modal-cancel is-wide-mobile"> Cancel <@button.submit id="DeleteUser" colour="primary is-wide-mobile"> Save Changes
================================================ FILE: src/main/resources/views/pages/error.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> ${error} ${exception} ================================================ FILE: src/main/resources/views/pages/home.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../prebuilt/base.ftl" as base /> <#import "../prebuilt/fleet-title.ftl" as title /> <#import "../prebuilt/image-list-table-item.ftl" as imageListTableItem /> <#import "../prebuilt/image-list-item.ftl" as imageListBoxItem /> <#import "../ui/form/input.ftl" as input /> <#import "../ui/components/message.ftl" as message /> <#import "../ui/elements/box.ftl" as box /> <#import "../ui/elements/table.ftl" as table /> <#import "../ui/elements/button.ftl" as button /> <@base.base title="Images" context="home" showTitle=false availableRepositories=availableRepositories>
<#if selectedRepository?has_content>
<@title.title icon="cubes" boldValue=selectedRepository.name />
<@input.text id="SearchImages" icon="search" placeholder="Search..." />
<#if selectedRepository.images?has_content>
<@table.table id="ImageTable" isFullWidth=true isScrollable=true extraClasses="table--sortable"> Name Latest Version Pulls Stars Build Time <#list selectedRepository.images as image> <@imageListTableItem.imageListItem image=image />
<#else>
No images
================================================ FILE: src/main/resources/views/pages/image.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../prebuilt/base.ftl" as base /> <#import "../prebuilt/fleet-title.ftl" as title /> <#import "../prebuilt/docker-example.ftl" as dockerExample /> <#import "../ui/components/message.ftl" as message /> <#import "../ui/elements/box.ftl" as box /> <#import "../ui/elements/button.ftl" as button /> <#import "../ui/elements/table.ftl" as table /> <#import "../ui/elements/tag.ftl" as tag /> <#import "../ui/layout/container.ftl" as container /> <#import "../ui/layout/section.ftl" as section /> <@base.base title="${(image.fullName)!'Unknown Image'}" context="image" hasHero=false> <#if image?has_content>
<@section.section extraClasses="is-paddingless-bottom"> <@container.container extraClasses="has-margin-bottom">
<@title.title icon="cube" imageIcon=image.metaData.appImagePath thinValue=image.repositoryName boldValue=image.name separator="/" subtitle=image.description />
<@tag.tag colour="light" value=' ${image.pullCount}' extraAttributes='title="Pulls"' /> <@tag.tag colour="light" value=' ${image.starCount}' extraAttributes='title="Stars"' /> <#assign latestBranch=image.findTagBranchByName("latest") /> <#if latestBranch?has_content> <#list latestBranch.latestTag.digests as digest> <@tag.tag colour="light" value=' ${digest.architecture}' extraAttributes='title="Architecture"' /> <#if image.deprecated> <@tag.tag colour="warning" value="Deprecated" /> <#if !image.stable> <@tag.tag colour="danger" value="Unstable" />
<@section.section> <@container.container>

Build Information

General build information for this image

<@table.table isFullWidth=true isNarrow=false isStriped=true isScrollable=true> <@table.halfDisplayRow title="Docker Hub" value=' ${image.fullName}' link="https://hub.docker.com/r/${image.fullName}" /> <@table.halfDisplayRow title="Build Time" value=image.lastUpdatedAsString /> <#if image.metaData.baseImage?has_content> <@table.halfDisplayRow title="Base Image" value=image.metaData.baseImage?html /> <#if image.metaData.category?has_content> <@table.halfDisplayRow title="Category" value=image.metaData.category?html /> <@table.halfDisplayRow title="Synchronised" value=image.syncEnabled?string("Yes", "No") /> <@table.halfDisplayRow title="Stable" value=image.stable?string("Yes", "No") /> <@table.halfDisplayRow title="Deprecated" value=image.deprecated?string("Yes", "No") />
<#if image.metaData.populated || image.metaData.externalUrls?has_content>

Support Information

External links and support

<@table.table isFullWidth=true isNarrow=false isStriped=true isScrollable=true> <#-- Any set external Urls --> <#list image.metaData.externalUrls as url> <@table.halfDisplayRow title=' ${url.name}' value=url.absoluteUrl link=url.absoluteUrl />

Tracked Tags

Known tags which link to a specific branched app version.

<@table.table isFullWidth=true isNarrow=false isStriped=true isScrollable=true> Branch Version Built <#list image.tagBranches as tagBranch> ${tagBranch.branchName?html} ${image.getMaskedVersion(tagBranch.latestTag)} ${formatDate(tagBranch.latestTag.buildDate, "dd MMM yyyy 'at' HH:mm:ss")}

Daily Pull Statistics

Running this as a container

Basic examples for getting this image running as a container

<#-- This is not ready yet <@message.message colour="info"> These examples do not include the relevant values for volume mappings or environment variables. You will need to review these snippets and fill in the gaps based on your own needs. If you would like to generate a compose block or CLI run command with your mappings included, you can also use the template generator:
<@button.link id="TemplateGeneratorLink" colour="info"> Template Generator
-->

Docker Compose

<@dockerExample.compose fullName=image.fullName containerName=image.name templates=image.metaData.templates latest=image.latestTag.version />

CLI

<@dockerExample.cli fullName=image.fullName containerName=image.name templates=image.metaData.templates latest=image.latestTag.version />
<#else> <@section.section> <@container.container> Could not find image. ================================================ FILE: src/main/resources/views/pages/login.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../prebuilt/base.ftl" as base /> <#import "../prebuilt/fleet-title.ftl" as title /> <#import "../ui/elements/box.ftl" as box /> <#import "../ui/form/input.ftl" as input /> <#import "../ui/elements/button.ftl" as button /> <#import "../ui/components/message.ftl" as message /> <#import "../ui/layout/container.ftl" as container /> <@base.base title="Log In" showNav=false context="login" hasHero=false>
<@container.container isFluid=true>
<@box.box> <@title.title boldValue="fleet" /> <@input.text id="username" icon="user" placeholder="Username" isRequired=true /> <@input.password id="password" icon="lock" placeholder="Password" isRequired=true /> <@button.submit id="SubmitLogin" extraClasses="is-fullwidth"> Log In <#if loginFailed?has_content> <@message.message colour="danger" extraClasses="has-margin-top"> ${loginFailed?html}
================================================ FILE: src/main/resources/views/prebuilt/base.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../ui/components/navbar.ftl" as navbar /> <#import "../ui/layout/container.ftl" as container /> <#import "../ui/elements/button.ftl" as button /> <#import "../ui/form/input.ftl" as input /> <#import "../prebuilt/system-alert.ftl" as systemAlert /> <#macro base title context showTitle=true showNav=true backgroundColour="white" hasHero=false availableRepositories=[]> <#if showTitle>${title} | </#if>fleet <#if showNav> <@navbar.navbar id="MainNav" hasShadow=false itemPlacement="end" colour=(hasHero?string('dark', 'normal-colour')) extraClasses=''> <#if context="home"> <#if __AuthenticatedUser?has_content> <@navbar.dropdown icon="shield-alt" displayText="Admin" isRight=true> <@navbar.item displayText="Repositories" icon="cubes" link="/admin/repositories" isActive=(context=='admin_repositories') /> <@navbar.item displayText="Schedules" icon="clock" link="/admin/schedules" isActive=(context=='admin_schedules') /> <@navbar.item displayText="Users" icon="users" link="/admin/users" isActive=(context=='admin_users') /> <@navbar.dropdown displayText=__AuthenticatedUser.name isRight=true icon="user"> <@navbar.item displayText="Log Out" icon="sign-out-alt" icon="sign-out-alt" link="/login?invalidate=true" /> <#else> <@navbar.buttons size="small"> <@button.link id="LogIn" link="/login" size="small" colour="light"> Log In
<#if __SystemAlerts?has_content && __SystemAlerts?size > 0> <@container.container isFluid=true> <#list __SystemAlerts as alert> <@systemAlert.alert specificAlert=alert /> <#nested> <#if context?starts_with("admin")> ================================================ FILE: src/main/resources/views/prebuilt/docker-example.ftl ================================================ <#-- Copyright (c) 2020 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#macro compose fullName containerName templates latest="">
---
version: "2"
services:
  ${containerName}:
    image: ${fullName}<#if latest?has_content>:${latest}
    container_name: ${containerName}
<#if templates.hostNetworkingEnabled>    network_mode: host

<#if templates.restartPolicy?has_content>
    restart: ${templates.restartPolicy}
<#if templates.capabilities?has_content>    cap_add:
    <#list templates.capabilities as cap>
        - ${cap}
    

<#if templates.env?has_content>    environment:
  <#list templates.env as env>
      - ${env.name}<#if env.exampleValue?has_content>=${env.exampleValue}<#if env.description?has_content> # ${env.description}
  

<#if templates.volumes?has_content>    volumes:
  <#list templates.volumes as volume>
      - /host/path/to${volume.name}:${volume.name}<#if volume.readonly>:ro <#if volume.description?has_content># ${volume.description}
  

<#if templates.ports?has_content && !templates.hostNetworkingEnabled>    ports:
  <#list templates.ports as port>
      - ${port.name?string["##0"]}:${port.name?string["##0"]}/${port.protocol} <#if port.description?has_content># ${port.description}
  

<#if templates.devices?has_content>    devices:
  <#list templates.devices as device>
      - ${device.name}:${device.name} <#if device.description?has_content># ${device.description}
  
<#macro cli fullName containerName templates latest="">
docker create \
  --name=${containerName} \<#if templates.hostNetworkingEnabled>
  --net=host \<#if templates.env?has_content>
<#list templates.env as env>
  -e ${env.name}<#if env.exampleValue?has_content>=${env.exampleValue}<#if env.description?has_content> `# ${env.description}` \


<#if templates.volumes?has_content>
<#list templates.volumes as volume>
  -v /host/path/to${volume.name}:${volume.name}<#if volume.readonly>:ro<#if volume.description?has_content> `# ${volume.description}` \


<#if templates.ports?has_content && !templates.hostNetworkingEnabled>
<#list templates.ports as port>
  -p ${port.name?string["##0"]}:${port.name?string["##0"]}/${port.protocol}<#if port.description?has_content> `# ${port.description}` \


<#if templates.devices?has_content>
<#list templates.devices as device>
  --device ${device.name}:${device.name}<#if device.description?has_content> `# ${device.description}` \


<#if templates.capabilities?has_content>
<#list templates.capabilities as cap>
  --cap-add=${cap} \


<#if templates.restartPolicy?has_content>  --restart ${templates.restartPolicy} \
  ${fullName}<#if latest?has_content>:${latest}
================================================ FILE: src/main/resources/views/prebuilt/fleet-title.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#macro title thinValue="" boldValue="" separator="" icon="" imageIcon="" subtitle="">

<#if imageIcon?has_content>
Title logo
<#elseif icon?has_content> <#if thinValue?has_content>${thinValue}${separator}${boldValue}. <#nested />

<#if subtitle?has_content>

${subtitle}

================================================ FILE: src/main/resources/views/prebuilt/image-list-item.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../ui/elements/tag.ftl" as tag /> <#macro imageListItem image> <#if !image.hidden>

${image.repositoryKey.name}/${image.name}

${image.lastUpdatedAsString}
<#list image.tagBranches as tagBranch> <@tag.tag colour="light" value=' ${tagBranch.latestTag.version}' extraAttributes='title="Latest Version"' /> <@tag.tag colour="light" value=' ${image.pullCount}' extraAttributes='title="Pulls"' /> <@tag.tag colour="light" value=' ${image.starCount}' /> <#if image.deprecated> <@tag.tag colour="warning" value=' Deprecated' /> <#if !image.stable> <@tag.tag colour="danger" value=' Unstable!' />
================================================ FILE: src/main/resources/views/prebuilt/image-list-table-item.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../ui/elements/tag.ftl" as tag /> <#macro imageListItem image> <#if !image.hidden> <#if image.metaData.appImagePath?has_content>
Title logo
<#else>

${image.repositoryKey.name} / ${image.name}

<#if image.deprecated> <@tag.tag colour="warning is-light" value=' Deprecated' /> <#if !image.stable> <@tag.tag colour="danger is-light" value=' Unstable!' /> <@tag.tag colour="light" value=' ${image.latestTag.version}' extraAttributes='title="Latest Version"' /> ${image.pullCount} ${image.starCount} ${image.lastUpdatedAsString!""} ================================================ FILE: src/main/resources/views/prebuilt/system-alert.ftl ================================================ <#-- Copyright (c) 2019 LinuxServer.io This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . --> <#import "../ui/components/message.ftl" as message /> <#import "../prebuilt/fleet-title.ftl" as fleetTitle /> <#macro alert specificAlert> <@message.message colour="danger"> <@fleetTitle.title boldValue=specificAlert.subject /> ${specificAlert.alertMessage!""} ================================================ FILE: src/main/resources/views/ui/components/dropdown.ftl ================================================ <#macro dropdown triggerText id title="" size="normal" isDisabled=false extraClasses="" extraAttributes=""> <#macro item id="" link="" modal="" isActive=false extraClasses="" extraAttributes=""> id="${id}" class="dropdown-item<#if isActive> is-active<#if modal?has_content> is-modal-trigger<#if extraClasses?has_content> ${extraClasses}"<#if extraAttributes?has_content> ${extraAttributes}<#if modal?has_content> data-modal="${modal}"<#if link?has_content> href="${link}"> <#nested /> ================================================ FILE: src/main/resources/views/ui/components/message.ftl ================================================ <#-- Reference: https://bulma.io/documentation/components/message/ Colored message blocks, to emphasize part of your page title:String - An optional heading for the message. This affects the style slightly if provided. colour:enum(light, dark, danger, warning, info, success) - The colour scheme of the element. extraClasses:String - Any other classes the element should have. extraAttributes:String - Any other attributes (e.g. data-*) the element should have. --> <#macro message title="" colour="" extraClasses="" extraAttributes="">
${extraAttributes}> <#if title?has_content>
${title}
<#nested />
================================================ FILE: src/main/resources/views/ui/components/modal.ftl ================================================ <#-- Reference: https://bulma.io/documentation/components/modal/ A classic modal overlay, in which you can include any content you want id:String - An identifier for this element title:String - An optional title for the modal isDismissable:boolean - Whether this element can be closed by the user isWide:boolean - Sets the modal to take up a larger proportion of the screen. Default: true extraClasses:String - Any other classes the element should have. extraAttributes:String - Any other attributes (e.g. data-*) the element should have. --> <#import "../elements/box.ftl" as box /> <#macro modal id title="" isDismissable=true isWide=true extraClasses="" extraAttributes=""> ================================================ FILE: src/main/resources/views/ui/components/navbar.ftl ================================================ <#-- Resource: https://bulma.io/documentation/components/navbar/ Constructs a navigation bar for display (usually) at the top of the page. id:String - The unique identifier for this element hasShadow:boolean - Whether or not the nav should have a drop shadow. Default: true itemPlacement:enum("start", "end") - Informs the navbar where to place the items. extraClasses:String - Any additional classes that this element should have --> <#macro navbar id="" hasShadow=true itemPlacement="start" extraClasses="" colour="normal-colour"> <#-- Resource: https://bulma.io/documentation/components/navbar/ Constructs a single item within a navbar. id:String - The unique identifier for this element displayText:String - The text which will be viewable on the screen. This may be HTML. icon:String - An icon to display next to the text notificationCount:int - If there is a linked notification count assigned to this item, it will be displayed as a badge. extraClasses:String - Any additional classes that this element should have --> <#macro item displayText id="" link="" icon="" title="" isActive=false extraClasses=""> id="${id}" class="navbar-item is-relative<#if isActive> is-active<#if extraClasses?has_content> ${extraClasses}" <#if link?has_content>href="${link}"<#if title?has_content> title="${title}"> <#if icon?has_content> ${displayText} <#macro buttons isGrouped=false size="normal"> <#-- Resource: https://bulma.io/documentation/components/navbar/ Constructs a wrapper for a dropdown item, including its text. id:String - The unique identifier for this element displayText:String - The text which will be viewable on the screen. This may be HTML. icon:String - An icon to display next to the text isHoverable:boolean - Sets whether or not the contents of the dropdown are displayed when the mouse hovers over it. Default: true extraClasses:String - Any additional classes that this element should have --> <#macro dropdown displayText="" icon="" isHoverable=true isRight=false id="">
id="${id}" class="navbar-item has-dropdown<#if isHoverable> is-hoverable"> <#if icon?has_content> ${displayText}
================================================ FILE: src/main/resources/views/ui/components/pagination.ftl ================================================ <#-- Reference: https://bulma.io/documentation/components/pagination/ A responsive, usable, and flexible pagination isRight:boolean - If set to true, the list will be on the right currentPage:int - The current page to be highlighted allPages:list(int) - A list of all pages to render. A NULL value will be rendered as an ellipsis. additionalQueryParameters:String - Any additional params to be appended to the end of each link. --> <#macro pagination isRight=true currentPage=1 allPages=[] additionalQueryParameters=""> ================================================ FILE: src/main/resources/views/ui/elements/box.ftl ================================================ <#-- Resource: https://bulma.io/documentation/elements/box/ Constructs a visible container for content on a page. id:String - The unique identifier for this element extraClasses:String - Any additional classes that this element should have extraAttributes:String - Any additional attributes that this element should have (e.g. disabled, data-*) --> <#macro box id="" extraClasses="" extraAttributes="">
id="${id}" class="box<#if extraClasses?has_content> ${extraClasses}"<#if extraAttributes?has_content> ${extraAttributes}>
<#nested />
================================================ FILE: src/main/resources/views/ui/elements/button.ftl ================================================ <#-- Resource: https://bulma.io/documentation/elements/button/ The classic button, in different colors, sizes, and states id:String - The unique identifier for this element type:enum(button, submit) - Sets the type of button. If "submit", it will interact with its parent form. title:String - A description of the button to give further context. extraClasses:String - Any additional classes that this element should have extraAttributes:String - Any additional attributes that this element should have (e.g. disabled, data-*) colour:enum(primary, warning, info, danger, success, light, dark) - Sets the main colour of the button. Default: primary size:enum(small, normal, medium, large) - Sets the size of the button. isWide:boolean - If set to true, will fill available horizontal space. isOutlined:boolean - If set to true, will set colour on button outline rather than background. isInverted:boolean - If set to true, will set the text colour rather than background. isRounded:boolean - If set to true, will fully round the edges of the button. --> <#macro button id="" type="button" title="" extraClasses="" extraAttributes="" colour="primary" size="normal" modal="" isDisabled=false isWide=false isOutlined=false isInverted=false isRounded=false> <#-- A convenience macro to generate a submit button. --> <#macro submit id="" extraClasses="" title="" extraAttributes="" colour="primary" size="normal" isWide=false isOutlined=false isInverted=false isRounded=false> <@button id=id type="submit" title=title extraClasses=extraClasses extraAttributes=extraAttributes colour=colour size=size isWide=isWide isOutlined=isOutlined isInverted=isInverted isRounded=isRounded> <#nested /> <#-- Generates an anchor link with the same styling as a button. --> <#macro link id="" link="" title="" extraClasses="" extraAttributes="" colour="primary" size="normal" modal="" isWide=false isOutlined=false isInverted=false isRounded=false> href="${link}"<#if title?has_content> title="${title}" class="button is-${size} is-${colour}<#if isWide> is-fullwidth<#if isOutlined> is-outlined<#if isInverted> is-inverted<#if isRounded> is-rounded<#if extraClasses?has_content> ${extraClasses}<#if modal?has_content> is-modal-trigger"<#if modal?has_content> data-modal="${modal}"<#if extraAttributes?has_content> ${extraAttributes}> <#nested /> <#macro buttons isGrouped=false size="normal" isRightAligned=false>
<#nested />
================================================ FILE: src/main/resources/views/ui/elements/display-field.ftl ================================================ <#import "../../templates/helpers/type-safe.ftl" as typeSafe /> <#macro displayField value label="" size="normal" isInline=false extraClasses="" extraAttributes="">
${extraAttributes}> <#if label?has_content>
<@typeSafe.render value=value />
================================================ FILE: src/main/resources/views/ui/elements/media.ftl ================================================ <#-- Resource: https://bulma.io/documentation/elements/box/ Constructs a visible container for content on a page. id:String - The unique identifier for this element extraClasses:String - Any additional classes that this element should have extraAttributes:String - Any additional attributes that this element should have (e.g. disabled, data-*) --> <#macro media id="" extraClasses="" extraAttributes=""> id="${id}" class="media<#if extraClasses?has_content> ${extraClasses}"<#if extraAttributes?has_content> ${extraAttributes}>
<#nested />
================================================ FILE: src/main/resources/views/ui/elements/notification.ftl ================================================ <#-- Reference: https://bulma.io/documentation/elements/notification/ Bold notification blocks, to alert your users of something id:String - The unique identifier for this element isError:boolean - Colours the notification with an error-like colour isWarning:boolean - Colours the notification with a warning-like colour isSuccess:boolean - Colours the notification with a success-like colour isInfo:boolean - Colours the notification with an info-like colour isDismissable:boolean - Whether or not the notification may be removed via an "x" button. Default: true Note on colour flags: The notification will always take the colour of the highest priority level. Therefore if isWarning=true and isInfo=true, the resulting colour will be for isWarning as it has a higher priority. --> <#macro notification id="" isError=false isWarning=false isSuccess=false isInfo=true isDismissable=true> id="${id}" class="notification is-<#if isError>danger<#elseif isWarning>warning<#elseif isSuccess>success<#elseif isInfo>info<#else>primary"> <#if isDismissable> <#nested /> ================================================ FILE: src/main/resources/views/ui/elements/table.ftl ================================================ <#-- Resource: https://bulma.io/documentation/elements/table/ The inevitable HTML table, with special case cells id:String - The unique identifier for this element isBordered:boolean - Places a border around the outer edges of the table isStriped:boolean - Sets a background colour on all alternate rows isFullWidth:boolean - Ensures the table fills the available horizontal space of its parent isHoverable:boolean - Sets a hover colour for rows in the table isScrollable:boolean - Allows the table to be horizontally scrolled if too wide extraClasses:String - Any additional classes that this element should have extraAttributes:String - Any additional attributes that this element should have (e.g. disabled, data-*) --> <#macro table id="" isBordered=false isStriped=false isFullWidth=true isHoverable=false isNarrow=false isScrollable=false extraClasses="" extraAttributes=""> <#if isScrollable>
id="${id}" class="table<#if isNarrow> is-narrow<#if isBordered> is-bordered<#if isStriped> is-striped<#if isFullWidth> is-fullwidth<#if isHoverable> is-hoverable<#if extraClasses?has_content> ${extraClasses}"<#if extraAttributes?has_content> ${extraAttributes}> <#nested />
<#if isScrollable>
<#-- Builds a single row with two columns of equal length (50%). The left side contains the title while the right side contains the value. --> <#macro halfDisplayRow title value link="" id=""> ${title} id="${id}"> <#if link?has_content> ${value} <#if link?has_content> ================================================ FILE: src/main/resources/views/ui/elements/tag.ftl ================================================ <#-- Reference: https://bulma.io/documentation/elements/tag/ Small tag labels to insert anywhere value:String - The primary text to display in the tag colour:enum(primary, warning, info etc) - The colour of the main text background size:enum(small, normal, large) - The size of the tag isRounded:boolean - If true, rounds the edges. Only applied if isGrouped=false isGrouped:boolean - If true, generates a title for the tag groupTitle:String - A title for the tag, separate to the main text extraClasses:String - Any other classes the input should have. These are applied directly to the input extraAttributes:String - Any other attributes (e.g. data-*) the input should have. Applied directly to the input. --> <#macro tag value colour="primary" size="normal" isRounded=false isGrouped=false groupTitle="" extraClasses="" extraAttributes=""> <#if isGrouped>
${groupTitle} ${extraAttributes}> ${value} <#if isGrouped>
================================================ FILE: src/main/resources/views/ui/form/input.ftl ================================================ <#-- Reference: https://bulma.io/documentation/form/input/ type:enum(text, password, number, date) - Determines the type of input and how its value is displayed id:String - A unique identifier for this element label:String - If provided, a text label is displayed next to the input placeholder:String - Places a hint into the input extraClasses:String - Any other classes the input should have. These are applied directly to the input extraAttributes:String - Any other attributes (e.g. data-*) the input should have. Applied directly to the input. icon:String - An optional icon to be displayed at the front of the input. required:boolean - Marks the input as being required if part of a form. Default: false readonly:boolean - Marks the input as being readonly. This will ensure the value cannot be changed. disabled:boolean - Marks the input as disabled. This will not only block value changes but will also omit the input from any form submissions. isInvalid:boolean - If the value is already known to be invalid at the time of rendering, it will display the requiredHelp if any is provided requiredHelp:String - If "required" is true, some help text to be displayed size:enum(small, normal, large) - The size of the input --> <#macro input type id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" infoText="" size="normal">
<#if isInline>
<#if label?has_content>
<#if label?has_content && !isInline>
title="${title}" class="input<#if isReadonly> is-static is-${size}<#if extraClasses?has_content> ${extraClasses}" id="${id}" name="${id}"<#if isRequired> required<#if isReadonly> readonly<#if isDisabled> disabled<#if value?has_content> value="${value?html}"<#if placeholder?has_content> placeholder="${placeholder}"<#if extraAttributes?has_content> ${extraAttributes}/> <#if icon?has_content> <#if isRequired && requiredHelp?has_content>

${requiredHelp}

<#if infoText?has_content>

${infoText}

<#if isInline>
<#-- Reference: https://bulma.io/documentation/form/input/ Convenience macro to generate a text input id:String - A unique identifier for this element label:String - If provided, a text label is displayed next to the input placeholder:String - Places a hint into the input extraClasses:String - Any other classes the input should have. These are applied directly to the input extraAttributes:String - Any other attributes (e.g. data-*) the input should have. Applied directly to the input. icon:String - An optional icon to be displayed at the front of the input. required:boolean - Marks the input as being required if part of a form. Default: false readonly:boolean - Marks the input as being readonly. This will ensure the value cannot be changed. disabled:boolean - Marks the input as disabled. This will not only block value changes but will also omit the input from any form submissions. requiredHelp:String - If "required" is true, some help text to be displayed size:enum(small, normal, large) - The size of the input --> <#macro text id value="" label="" title="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" infoText="" size="normal"> <@input type="text" id=id value=value title=title label=label placeholder=placeholder extraAttributes=extraAttributes extraClasses=extraClasses icon=icon isRequired=isRequired isReadonly=isReadonly isDisabled=isDisabled isInvalid=isInvalid isInline=isInline requiredHelp=requiredHelp infoText=infoText size=size /> <#-- Reference: https://bulma.io/documentation/form/input/ Convenience macro to generate a password input id:String - A unique identifier for this element label:String - If provided, a text label is displayed next to the input placeholder:String - Places a hint into the input extraClasses:String - Any other classes the input should have. These are applied directly to the input extraAttributes:String - Any other attributes (e.g. data-*) the input should have. Applied directly to the input. icon:String - An optional icon to be displayed at the front of the input. required:boolean - Marks the input as being required if part of a form. Default: false readonly:boolean - Marks the input as being readonly. This will ensure the value cannot be changed. disabled:boolean - Marks the input as disabled. This will not only block value changes but will also omit the input from any form submissions. requiredHelp:String - If "required" is true, some help text to be displayed size:enum(small, normal, large) - The size of the input --> <#macro password id value="" label="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" size="normal"> <@input type="password" id=id value=value label=label placeholder=placeholder extraAttributes=extraAttributes extraClasses=extraClasses icon=icon isRequired=isRequired isReadonly=isReadonly isDisabled=isDisabled isInvalid=isInvalid isInline=isInline requiredHelp=requiredHelp size=size /> <#-- Reference: https://bulma.io/documentation/form/input/ Convenience macro to generate a number input id:String - A unique identifier for this element label:String - If provided, a text label is displayed next to the input placeholder:String - Places a hint into the input extraClasses:String - Any other classes the input should have. These are applied directly to the input extraAttributes:String - Any other attributes (e.g. data-*) the input should have. Applied directly to the input. icon:String - An optional icon to be displayed at the front of the input. required:boolean - Marks the input as being required if part of a form. Default: false readonly:boolean - Marks the input as being readonly. This will ensure the value cannot be changed. disabled:boolean - Marks the input as disabled. This will not only block value changes but will also omit the input from any form submissions. requiredHelp:String - If "required" is true, some help text to be displayed size:enum(small, normal, large) - The size of the input --> <#macro number id value="" label="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false isInline=false requiredHelp="" size="normal"> <@input type="number" id=id value=value label=label placeholder=placeholder extraAttributes=extraAttributes extraClasses=extraClasses icon=icon isRequired=isRequired isReadonly=isReadonly isDisabled=isDisabled isInvalid=isInvalid isInline=isInline requiredHelp=requiredHelp size=size /> <#macro dropdown id label="" extraClasses="" extraAttributes="" icon="" colour="" isInline=false isMultiple=false isRequired=false isReadonly=false isDisabled=false requiredHelp="" size="normal">
<#if label?has_content>
<#if icon?has_content>
<#if isRequired && requiredHelp?has_content>
<#-- Reference: https://bulma.io/documentation/form/input/ Creates a neat toggle element on the page in the place of a checkbox. id:String - A unique identifier for this element title:String - If provided, a text label is displayed next to the input toggled:boolean - The initial toggle state of the toggle. extraClasses:String - Any other classes the input should have. These are applied to the parent span inputClasses:String - Any other classes the input should have. These are applied directly to the input extraAttributes:String - Any other attributes (e.g. data-*) the input should have. Applied directly to the input. disabled:boolean - Marks the input as disabled. This will not only block value changes but will also omit the input from any form submissions. colour:enum(danger, warning, info, primary, link) - The colour the toggle will be when selected. size:enum(small, normal, large) - The size of the input --> <#macro toggle id="" title="" label="" isToggled=false isDisabled=false size="normal" colour="primary" inputClasses="" extraClasses="" extraAttributes=""> <#if label?has_content>
<#if label?has_content>
<#-- Reference: https://bulma.io/documentation/form/input/ Creates a basic text input with a surrounding container which also holds an autocomplete placeholder. You will need to provide the relevant logic to populate this placeholder. id:String - A unique identifier for this element title:String - If provided, a text label is displayed next to the input toggled:boolean - The initial toggle state of the toggle. extraClasses:String - Any other classes the input should have. These are applied to the parent span inputClasses:String - Any other classes the input should have. These are applied directly to the input extraAttributes:String - Any other attributes (e.g. data-*) the input should have. Applied directly to the input. disabled:boolean - Marks the input as disabled. This will not only block value changes but will also omit the input from any form submissions. colour:enum(danger, warning, info, primary, link) - The colour the toggle will be when selected. size:enum(small, normal, large) - The size of the input --> <#macro autocomplete id value="" label="" placeholder="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false requiredHelp="" size="normal">
<@text id=id value=value label=label placeholder=placeholder extraClasses=extraClasses extraAttributes=extraAttributes icon=icon isRequired=isRequired isReadonly=isReadonly isDisabled=isDisabled isInvalid=isInvalid requiredHelp=requiredHelp size=size />
<#-- Reference: https://bulma.io/documentation/form/input/ Creates a placeholder field with the ability to "switch" it into an input which can then be actioned. id:String - A unique identifier for this element title:String - If provided, a text label is displayed next to the input acceptClass:String - A classifier to be attached to the "Accept" button. Useful for JS triggering. isSwitched:boolean - If true, the input will be displayed initially rather than the placeholder extraClasses:String - Any other classes the input should have. These are applied to the parent span inputClasses:String - Any other classes the input should have. These are applied directly to the input extraAttributes:String - Any other attributes (e.g. data-*) the input should have. Applied directly to the input. disabled:boolean - Marks the input as disabled. This will not only block value changes but will also omit the input from any form submissions. colour:enum(danger, warning, info, primary, link) - The colour the toggle will be when selected. size:enum(small, normal, large) - The size of the input --> <#macro switchable id isSwitched=false value="" placeholder="" acceptClass="" extraClasses="" extraAttributes="" icon="" isRequired=false isReadonly=false isDisabled=false isInvalid=false requiredHelp="" size="normal">
required<#if isReadonly> readonly<#if isDisabled> disabled<#if value?has_content> value="${value}" placeholder="${placeholder}"<#if extraAttributes?has_content> ${extraAttributes}/> <#if icon?has_content>
<#if isRequired && requiredHelp?has_content>

${requiredHelp}

<#if value?has_content> ${value} <#else>
================================================ FILE: src/main/resources/views/ui/layout/container.ftl ================================================ <#-- Resource: https://bulma.io/documentation/layout/container/ A simple container to center your content horizontally id:String - The unique identifier for this element extraClasses:String - Any additional classes that this element should have isFluid:boolean - Defines whether or not the container becomes "full width". Default: false --> <#macro container id="" extraClasses="" isFluid=false>
id="${id}" class="container<#if isFluid> is-fluid<#if extraClasses?has_content> ${extraClasses}"> <#nested />
================================================ FILE: src/main/resources/views/ui/layout/footer.ftl ================================================ <#-- Reference: https://bulma.io/documentation/layout/footer/ A simple responsive footer which can include anything: lists, headings, columns, icons, buttons, etc. serviceInfo:AtassServiceInfo - An optional param which will be used to display the service name and other build info --> <#macro footer serviceInfo="">
<#if serviceInfo?has_content> ${serviceInfo.serviceFullName}

${serviceInfo.version}

================================================ FILE: src/main/resources/views/ui/layout/hero.ftl ================================================ <#-- Resource: https://bulma.io/documentation/layout/hero/ An imposing hero banner to showcase something id:String - The unique identifier for this element isFullHeight:boolean - Ensures the hero fills the entire screen extraClasses:String - Any additional classes that this element should have --> <#macro hero id="" isFullHeight=false extraClasses="">
id="${id}" class="hero<#if isFullHeight> is-fullheight<#if extraClasses?has_content> ${extraClasses}">
<#nested />
================================================ FILE: src/main/resources/views/ui/layout/section.ftl ================================================ <#-- Resource: https://bulma.io/documentation/layout/ection/ A simple container to divide your page into sections id:String - The unique identifier for this element extraClasses:String - Any additional classes that this element should have --> <#macro section id="" extraClasses="">
id="${id}" class="section<#if extraClasses?has_content> ${extraClasses}"> <#nested />
================================================ FILE: src/test/java/io/linuxserver/fleet/auth/security/PBKDF2PasswordEncoderTest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.auth.security; import org.junit.Test; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class PBKDF2PasswordEncoderTest { private static final String HASH_FOR_PASSWORD = "AEGvVmpwYMMYdW5XkX5lqQWKsuZp0fSIxo27KUaI7nPqHF1gTVFvKlEIJyN9FJWsBG5bF59v5axUg2gpyqAsNbT7l9nT5KwKenfkcx7IVEo="; private PBKDF2PasswordEncoder encoder = new PBKDF2PasswordEncoder("superSecret"); @Test public void shouldGenerateHash() { assertThat(encoder.matches("password", HASH_FOR_PASSWORD), is(equalTo(true))); } } ================================================ FILE: src/test/java/io/linuxserver/fleet/dockerhub/util/DockerTagFinderTest.java ================================================ /* * Copyright (c) 2020 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.dockerhub.util; import io.linuxserver.fleet.v2.types.docker.DockerTag; import io.linuxserver.fleet.v2.types.docker.DockerTagManifestDigest; import org.junit.Before; import org.junit.Test; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class DockerTagFinderTest { @Test public void shouldFindCorrectTagIfAllDigestsMatch() { final DockerTag named = new DockerTag("latest", 1234L, LocalDateTime.now()); named.addDigest(new DockerTagManifestDigest(1234L, "digest1", "arch", "variant1")); named.addDigest(new DockerTagManifestDigest(1234L, "digest2", "arch", "variant2")); named.addDigest(new DockerTagManifestDigest(1234L, "digest3", "arch", "variant3")); final DockerTag shouldMatch = new DockerTag("v1234", 1234L, LocalDateTime.now()); shouldMatch.addDigest(new DockerTagManifestDigest(1234L, "digest1", "arch", "variant1")); shouldMatch.addDigest(new DockerTagManifestDigest(1234L, "digest2", "arch", "variant2")); shouldMatch.addDigest(new DockerTagManifestDigest(1234L, "digest3", "arch", "variant3")); final DockerTag shouldNotMatch = new DockerTag("v1234", 1234L, LocalDateTime.now()); shouldNotMatch.addDigest(new DockerTagManifestDigest(1234L, "digest1", "arch", "variant1")); shouldNotMatch.addDigest(new DockerTagManifestDigest(1234L, "digest4", "arch", "variant2")); shouldNotMatch.addDigest(new DockerTagManifestDigest(1234L, "digest3", "arch", "variant3")); final List tags = new ArrayList<>(); tags.add(named); tags.add(shouldMatch); tags.add(shouldNotMatch); assertThat(DockerTagFinder.findVersionedTagMatchingBranch(tags, "latest"), is(equalTo(shouldMatch))); } @Test public void shouldReturnFoundNamedTagIfNoOthersMatchFully() { final DockerTag named = new DockerTag("latest", 1234L, LocalDateTime.now()); named.addDigest(new DockerTagManifestDigest(1234L, "digest1", "arch", "variant1")); named.addDigest(new DockerTagManifestDigest(1234L, "digest2", "arch", "variant2")); named.addDigest(new DockerTagManifestDigest(1234L, "digest3", "arch", "variant3")); final DockerTag shouldMatch = new DockerTag("v1234", 1234L, LocalDateTime.now()); shouldMatch.addDigest(new DockerTagManifestDigest(1234L, "digest1", "arch", "variant1")); shouldMatch.addDigest(new DockerTagManifestDigest(1234L, "digest5", "arch", "variant2")); shouldMatch.addDigest(new DockerTagManifestDigest(1234L, "digest3", "arch", "variant3")); final DockerTag shouldNotMatch = new DockerTag("v1234", 1234L, LocalDateTime.now()); shouldNotMatch.addDigest(new DockerTagManifestDigest(1234L, "digest1", "arch", "variant1")); shouldNotMatch.addDigest(new DockerTagManifestDigest(1234L, "digest4", "arch", "variant2")); shouldNotMatch.addDigest(new DockerTagManifestDigest(1234L, "digest3", "arch", "variant3")); final List tags = new ArrayList<>(); tags.add(named); tags.add(shouldMatch); tags.add(shouldNotMatch); assertThat(DockerTagFinder.findVersionedTagMatchingBranch(tags, "latest"), is(equalTo(named))); } } ================================================ FILE: src/test/java/io/linuxserver/fleet/v2/thread/schedule/TimeWithUnitTest.java ================================================ /* * Copyright (c) 2019 LinuxServer.io * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package io.linuxserver.fleet.v2.thread.schedule; import org.junit.Test; import java.util.concurrent.TimeUnit; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; public class TimeWithUnitTest { @Test public void shouldConvertToLowestUnit() { final TimeWithUnit minutes = new TimeWithUnit(30, TimeUnit.MINUTES); final TimeWithUnit hours = new TimeWithUnit(3, TimeUnit.HOURS); assertThat(minutes.convertToLowestUnit(hours).getTimeUnit(), is(equalTo(TimeUnit.MINUTES))); assertThat(minutes.convertToLowestUnit(hours).getTimeDuration(), is(equalTo(30L))); assertThat(hours.convertToLowestUnit(minutes).getTimeUnit(), is(equalTo(TimeUnit.MINUTES))); assertThat(hours.convertToLowestUnit(minutes).getTimeDuration(), is(equalTo(180L))); } }