├── .gitignore ├── LICENSE ├── NUGG_Z_BTC_UI5_UPLOAD.nugg ├── README.md ├── doc ├── SICF.png └── SICF_service_properties.png ├── src ├── App.config ├── Cloud_Arrow_Up.ico ├── CredentialStore.cs ├── Credentials.cs ├── Crypto.cs ├── Engine.cs ├── Log.cs ├── NotAuthorizedException.cs ├── PackageCommand.cs ├── Program.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── SetCredentialsCommand.cs ├── UI5Uploader.csproj ├── UI5Uploader.sln ├── UploadCommand.cs ├── UploadFailedException.cs ├── UriHelper.cs ├── ZipHelper.cs └── packages.config └── z_btc_ui5_upload_webservice.abap /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | *.sap 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | 154 | # Windows Azure Build Output 155 | csx/ 156 | *.build.csdef 157 | 158 | # Windows Store app package directory 159 | AppPackages/ 160 | 161 | # Visual Studio cache files 162 | # files ending in .cache can be ignored 163 | *.[Cc]ache 164 | # but keep track of directories ending in .cache 165 | !*.[Cc]ache/ 166 | 167 | # Others 168 | ClientBin/ 169 | [Ss]tyle[Cc]op.* 170 | ~$* 171 | *~ 172 | *.dbmdl 173 | *.dbproj.schemaview 174 | *.pfx 175 | *.publishsettings 176 | node_modules/ 177 | orleans.codegen.cs 178 | 179 | # RIA/Silverlight projects 180 | Generated_Code/ 181 | 182 | # Backup & report files from converting an old project file 183 | # to a newer Visual Studio version. Backup files are not needed, 184 | # because we have git ;-) 185 | _UpgradeReport_Files/ 186 | Backup*/ 187 | UpgradeLog*.XML 188 | UpgradeLog*.htm 189 | 190 | # SQL Server files 191 | *.mdf 192 | *.ldf 193 | 194 | # Business Intelligence projects 195 | *.rdl.data 196 | *.bim.layout 197 | *.bim_*.settings 198 | 199 | # Microsoft Fakes 200 | FakesAssemblies/ 201 | 202 | # Node.js Tools for Visual Studio 203 | .ntvs_analysis.dat 204 | 205 | # Visual Studio 6 build log 206 | *.plg 207 | 208 | # Visual Studio 6 workspace options file 209 | *.opt 210 | 211 | # Visual Studio LightSwitch build output 212 | **/*.HTMLClient/GeneratedArtifacts 213 | **/*.DesktopClient/GeneratedArtifacts 214 | **/*.DesktopClient/ModelManifest.xml 215 | **/*.Server/GeneratedArtifacts 216 | **/*.Server/ModelManifest.xml 217 | _Pvt_Extensions 218 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Bernhard Klefer, BTC AG Oldenburg, Germany 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /NUGG_Z_BTC_UI5_UPLOAD.nugg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | *"* use this source file for the definition and implementation of 6 | *"* local helper classes, interface definitions and type 7 | *"* declarations 8 | *"* use this source file for any type of declarations (class 9 | *"* definitions, interfaces or type declarations) you need for 10 | *"* components in the private section 11 | *"* use this source file for any macro definitions you need 12 | *"* in the implementation part of the class 13 | *"* use this source file for your ABAP unit test classes 14 | 15 | 16 | 17 | METHOD if_http_extension~handle_request. 18 | if server->request->get_method( ) = 'HEAD'. 19 | server->response->set_status( code = 200 reason = 'OK' ). 20 | return. 21 | endif. 22 | if server->request->get_method( ) <> 'POST'. 23 | server->response->set_status( code = 500 reason = 'Only POST supported.' ). 24 | return. 25 | endif. 26 | server->request->get_form_fields( CHANGING fields = url_parameters ). 27 | data(zip_url) = upload_zip_to_http_cache( server->request->get_data( ) ). 28 | 29 | DATA success TYPE char1. 30 | DATA log_messages TYPE string_table. 31 | try. 32 | CALL FUNCTION '/UI5/UI5_REPOSITORY_LOAD_HTTP' 33 | EXPORTING 34 | iv_url = zip_url 35 | iv_sapui5_application_name = get_url_parameter( `sapui5applicationname` ) 36 | iv_sapui5_application_desc = get_url_parameter( `sapui5applicationdescription` ) 37 | iv_package = conv devclass( get_url_parameter( `sapui5applicationpackage` ) ) 38 | iv_workbench_request = conv trkorr( get_url_parameter( `workbenchrequest` ) ) 39 | iv_external_code_page = get_url_parameter( `externalcodepage` ) 40 | iv_accept_unix_style_eol = switch #( get_url_parameter( `acceptunixstyleeol` ) when `true` then abap_true when `false` then abap_false else abap_undefined ) 41 | iv_delta_mode = switch #( get_url_parameter( `deltamode` ) when `true` then abap_true when `false` then abap_false else abap_undefined ) 42 | iv_test_mode = switch #( get_url_parameter( `testmode` ) when `true` then abap_true else abap_false ) 43 | IMPORTING 44 | ev_success = success 45 | ev_log_messages = log_messages. 46 | catch cx_root into data(e). 47 | success = 'E'. 48 | while e is bound. 49 | e->get_source_position( importing program_name = data(program) include_name = data(include) source_line = data(line) ). 50 | insert |{ cl_abap_classdescr=>get_class_name( e ) }: { e->get_text( ) } in { include } { program } line { line }| into table log_messages. 51 | e = e->previous. 52 | endwhile. 53 | endtry. 54 | case success. 55 | when 'E'. 56 | server->response->set_status( code = 500 reason = 'Installation of UI5 application failed.' ). 57 | when 'S'. 58 | server->response->set_status( code = 200 reason = 'Finished' ). 59 | when others. 60 | server->response->set_status( code = 206 reason = 'Finished with warnings' ). 61 | endcase. 62 | 63 | CONCATENATE LINES OF log_messages INTO data(log_string) SEPARATED BY cl_abap_char_utilities=>newline. 64 | server->response->set_cdata( data = log_string ). 65 | ENDMETHOD. 66 | 67 | 68 | 69 | 70 | METHOD get_url_parameter. 71 | IF line_exists( url_parameters[ name = i_parameter_name ] ). 72 | r_result = url_parameters[ name = i_parameter_name ]-value. 73 | ENDIF. 74 | ENDMETHOD. 75 | 76 | 77 | 78 | 79 | METHOD upload_zip_to_http_cache. 80 | DATA(cached_response) = NEW cl_http_response( add_c_msg = 1 ). 81 | cached_response->set_content_type( `application/zip` ). 82 | cached_response->set_data( i_zip_data ). 83 | cached_response->if_http_response~set_status( code = 200 reason = 'OK' ). 84 | cached_response->if_http_response~server_cache_expire_rel( zip_cache_ttl_s ). 85 | cl_wd_utilities=>construct_wd_url( EXPORTING application_name = `UI5Upload` IMPORTING out_absolute_url = r_url ). 86 | 87 | r_url = r_url && `/` && cl_system_uuid=>create_uuid_c32_static( ). 88 | cl_http_server=>server_cache_upload( response = cached_response url = r_url ). 89 | ENDMETHOD. 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI5 Uploader 2 | ##Command line app to upload a OpenUI5/SAPUI5 project to a ABAP Repository 3 | 4 | ## Who will find this tool useful? 5 | * Are you developing a OpenUI5/SAPUI5 application that will be hosted on your SAP Server? 6 | * You want to use a VCS like git or Subversion to collaborate? 7 | * You don't want to use Eclipse but Webstorm or Visual Studio? 8 | * You want to automate the deployment within you continuous integration System? 9 | 10 | So you cannot use the Eclipse "SAPUI5 ABAP Repository Team Provider". 11 | 12 | ## What is it? 13 | The UI5 Uploader is a command line tool that will upload your UI5 application to the ABAP Repository of your SAP System. 14 | It will do mostly the same job as the /UI5/UI5_REPOSITORY_LOAD-Report but you don't need a SAP GUI 15 | and you can easily integrate it into your build process or add a button to your favorite IDE. 16 | * Creates the UI5 App in the SAP repository if its not existing. 17 | * Delta-Upload. Only changed files are added to the transport request. 18 | * Parameters can be configured via config file and command line (command line trumps). 19 | * Single Sign On (if your SAP System supports it). 20 | * Test mode. See what would happen, which filed would be uploaded or deleted. 21 | 22 | The tool is a .NET Application written in C#. It is accessing the SAP system via a webservice. 23 | 24 | ## What do you need? 25 | * .NET Framework 4 (should also work with Mono) 26 | * A simple one-class-webservice on your SAP Development system. See Installation for details. 27 | 28 | ## How to run it? 29 | There tool accepts three commands which can be displayed by runing it without parameters: 30 | ``` 31 | >UI5Uploader 32 | UI5Uploader - Upload Open/SAPUI5 applications to a SAP ABAP repository 33 | Copyright (c) 2015 Bernhard Klefer, BTC AG. Read license file for details. 34 | Available commands are: 35 | password - Set and save the credentials for a SAP system 36 | upload - Upload UI5 Application to a SAP Backend 37 | help - For help with one of the above commands 38 | ``` 39 | 40 | ###Examples 41 | 42 | ``` 43 | >UI5Uploader upload -s dev:8000 -src d:\MyUi5App --AppName ZMyUi5App -u bernhard 44 | UI5Uploader - Upload Open/SAPUI5 applications to a SAP ABAP repository 45 | Copyright (c) 2015 Bernhard Klefer, BTC AG. Read license file for details. 46 | [09:35:57] System: http://dev:8000/sap/bc/ui5upload 47 | [09:35:57] Mandant: 100 48 | [09:35:57] Username: bernhard 49 | [09:35:57] Project folder: d:\MyUi5App\ 50 | [09:35:57] App name: ZMyUi5App 51 | [09:35:57] Package: $TMP 52 | [09:35:57] Transport: 53 | Do you want to upload? [Y/N] 54 | y 55 | [09:36:01] Installing App... 56 | [09:36:04] ### Begin of response ########################## 57 | ***** Upload der SAPUI5-Anwendung aus ZIP-Archiv in SAPUI5-ABAP-Repository ***** 58 | . 59 | * Standardmodus aktiv: Kurzprotokoll * 60 | . 61 | * Verbindung zu ZIP-Archiv mit SAPUI5-Anwendung wird hergestellt * 62 | . http://dev.mydomain.de:8000/sap/bc/webdynpro/sap/ui5upload/5254ED3D79171EE4B 63 | FBF3630FDBBFFFA 64 | 80 Dateien in Archiv gefunden. 65 | . 66 | [...] 67 | . 68 | * Bestehende SAPUI5-Anwendung ZMyUi5App wird aktualisiert * 69 | . 70 | * Abgeschlossen * 71 | [09:36:04] ### End of response ########################## 72 | [09:36:04] Upload successfull! 73 | ``` 74 | 75 | ## Installation of the required Webservice 76 | The webservice consists of a single ABAP class implementing the interface `if_http_extension` 77 | and a SICF node for which the class is registered as handler. 78 | 79 | **a)** 80 | Import the class and the SICF-Node via 81 | [SAPlink](http://wiki.scn.sap.com/wiki/display/ABAP/SAPlink+User+Documentation) from the nugget 82 | [NUGG_Z_BTC_UI5_UPLOAD.nugg](https://github.com/kleferbe/ui5uploader/blob/master/NUGG_Z_BTC_UI5_UPLOAD.nugg) 83 | 84 | *or* 85 | 86 | **b)** 87 | You can just copy/paste the code of the class 88 | [z_btc_ui5_upload_webservice.abap](https://github.com/kleferbe/ui5uploader/blob/master/z_btc_ui5_upload_webservice.abap) 89 | to your SAP system and create the SICF-Node manually: 90 | 91 | 1. Start transaction `SICF`. 92 | 2. Go to node default_host/sap/bc and choose "New Sub-Element" in its context menu and confirm the next message. 93 | ![SICF node tree](doc/SICF.png) 94 | 3. Enter `ui5upload` for the "Name of Service Element to Be Created". 95 | 4. In the service properties enter at least one Description. 96 | 5. Go to tab "Handler List" and add `Z_BTC_UI5_UPLOAD_WEBSERVICE` to the list. 97 | ![SICF service handlerlist](doc/SICF_service_properties.png) 98 | 99 | :warning: If you create the SICF node with a path differing from `/sap/bc/ui5upload`, 100 | you have to specify the whole path for the -s parameter. For example `-s http://dev:8001/my/custom/service` -------------------------------------------------------------------------------- /doc/SICF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleferbe/ui5uploader/d03acb00f665cfcfec374c039884918f870eef77/doc/SICF.png -------------------------------------------------------------------------------- /doc/SICF_service_properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleferbe/ui5uploader/d03acb00f665cfcfec374c039884918f870eef77/doc/SICF_service_properties.png -------------------------------------------------------------------------------- /src/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | %AppData%\BTC\SAPUI5AppUpload\credentials 16 | 17 | 18 | 19 | 20 | git.exe 21 | 22 | 23 | %AppData%\BTC\SAPUI5AppUpload\credentials 24 | 25 | 26 | %ProgramFiles%\7-Zip\7z.exe 27 | 28 | 29 | a {0} {1} -xr!WEB-INF -xr!META-INF 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Cloud_Arrow_Up.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kleferbe/ui5uploader/d03acb00f665cfcfec374c039884918f870eef77/src/Cloud_Arrow_Up.ico -------------------------------------------------------------------------------- /src/CredentialStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Xml.Serialization; 10 | 11 | namespace BTC.UI5Uploader 12 | { 13 | /// 14 | /// Stores a collection of SAP system credentials in a XML file. 15 | /// 16 | [XmlType("CredentialStore")] 17 | public class CredentialStore 18 | { 19 | /// 20 | /// Creates a new and empty instance. Use to load stored credentials into the instance. 21 | /// 22 | public CredentialStore() 23 | { 24 | XMLCredentials = new List(); 25 | } 26 | 27 | /// 28 | /// The list of credentials for XML Serialization 29 | /// 30 | [XmlElement("Credentials")] 31 | public List XMLCredentials { get; set; } 32 | 33 | /// 34 | /// Searches the store for specific credentials 35 | /// 36 | /// Url of the SAP systems upload service 37 | /// SAP mandant of the 38 | /// Username 39 | /// Credentials of the specified user of the specified system or null, if no credentials could be found in this store. 40 | public Credentials GetCredentials(string system, int mandant, string username) 41 | { 42 | return XMLCredentials.FirstOrDefault(c => c.System.Equals(system, StringComparison.OrdinalIgnoreCase) && c.Mandant == mandant && c.Username.Equals(username, StringComparison.OrdinalIgnoreCase)); 43 | } 44 | /// 45 | /// Removes the specified credentials from this store. Please remember to call to persist the changes. 46 | /// 47 | /// Url of the SAP systems upload service 48 | /// SAP mandant of the 49 | /// Username 50 | /// true, if the credentials were found and have been removed. 51 | public bool RemoveCredentials(string system, int mandant, string username) 52 | { 53 | var old = GetCredentials(system, mandant, username); 54 | if (old != null) XMLCredentials.Remove(old); 55 | return old != null; 56 | } 57 | /// 58 | /// Adds the given credentials to the store or updates the credentials if an entry exists for the username, mandant and system. 59 | /// 60 | /// The credentials to add 61 | public void SetCredentials(Credentials newCredentials) 62 | { 63 | RemoveCredentials(newCredentials.System, newCredentials.Mandant, newCredentials.Username); 64 | XMLCredentials.Add(newCredentials); 65 | } 66 | 67 | static readonly XmlSerializer serializer = new XmlSerializer(typeof(CredentialStore)); 68 | /// 69 | /// Loads the credential store from the Credentials.xml file located in the folder specified in the CredentialFiles application setting. 70 | /// 71 | /// Loaded store or empty store if the file does not exist. 72 | public static CredentialStore Load() 73 | { 74 | var credentialPath = Environment.ExpandEnvironmentVariables(UI5Uploader.Properties.Settings.Default.CredentialFiles); 75 | Directory.CreateDirectory(credentialPath); 76 | var credentialFile = Path.Combine(credentialPath, "Credentials.xml"); 77 | var result = new CredentialStore(); 78 | if (File.Exists(credentialFile)) 79 | { 80 | result.Load(credentialFile); 81 | } 82 | else 83 | { 84 | Log.Instance.Write("New Credential Store created."); 85 | } 86 | return result; 87 | } 88 | /// 89 | /// Loads the credential store from the specified file. 90 | /// 91 | /// File to load the contents of this credential store from. 92 | public void Load(string fileName) 93 | { 94 | using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read)) 95 | { 96 | var store = (CredentialStore)serializer.Deserialize(stream); 97 | this.XMLCredentials = store.XMLCredentials; 98 | } 99 | } 100 | /// 101 | /// Saves the credential store to the specified file. 102 | /// 103 | /// File to save the contents of this credential store to. 104 | public void Save(string fileName) 105 | { 106 | using (var stream = new MemoryStream()) 107 | { 108 | serializer.Serialize(stream, this); 109 | File.WriteAllBytes(fileName, stream.ToArray()); 110 | } 111 | } 112 | /// 113 | /// Saves the credential store to the Credentials.xml file located in the folder specified in the CredentialFiles application setting. 114 | /// 115 | public void Save() 116 | { 117 | var credentialPath = Environment.ExpandEnvironmentVariables(UI5Uploader.Properties.Settings.Default.CredentialFiles); 118 | Directory.CreateDirectory(credentialPath); 119 | var credentialFile = Path.Combine(credentialPath, "Credentials.xml"); 120 | Save(credentialFile); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Credentials.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using System; 5 | using System.IO; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Xml.Serialization; 10 | 11 | namespace BTC.UI5Uploader 12 | { 13 | /// 14 | /// Credentials for a SAP System 15 | /// 16 | [XmlType("UI5UploadCredentials")] 17 | public class Credentials 18 | { 19 | /// 20 | /// Empty constructor used by XML deserialization 21 | /// 22 | public Credentials() { } 23 | /// 24 | /// Creates a new Credential-Container instance 25 | /// 26 | /// Url of the SAP systems upload service 27 | /// SAP mandant of the 28 | /// Username 29 | public Credentials(string system, int mandant, string username) 30 | { 31 | this.System = system; 32 | this.Mandant = mandant; 33 | this.Username = username; 34 | } 35 | /// 36 | /// Url of the SAP systems upload service 37 | /// 38 | [XmlAttribute] 39 | public string System { get; set; } 40 | /// 41 | /// SAP Mandant of the 42 | /// 43 | [XmlAttribute] 44 | public int Mandant { get; set; } 45 | /// 46 | /// Username 47 | /// 48 | [XmlAttribute] 49 | public string Username { get; set; } 50 | /// 51 | /// Password 52 | /// 53 | [XmlIgnore] 54 | public string Password { get; set; } 55 | /// 56 | /// The encrypted Password for XML serialization 57 | /// 58 | [XmlText] 59 | public string EncryptedPassword 60 | { 61 | get 62 | { 63 | return Crypto.EncryptStringAES(Password, "cc54b3a5473b74817480425ab2191537483cb9af7ddd3726914912bdaabdc7e7"); 64 | } 65 | set 66 | { 67 | Password = Crypto.DecryptStringAES(value, "cc54b3a5473b74817480425ab2191537483cb9af7ddd3726914912bdaabdc7e7"); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Crypto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | 8 | namespace BTC.UI5Uploader 9 | { 10 | public class Crypto 11 | { 12 | private static byte[] _salt = Encoding.ASCII.GetBytes("o6806642kbM7c5"); 13 | 14 | /// 15 | /// Encrypt the given string using AES. The string can be decrypted using 16 | /// DecryptStringAES(). The sharedSecret parameters must match. 17 | /// 18 | /// The text to encrypt. 19 | /// A password used to generate a key for encryption. 20 | public static string EncryptStringAES(string plainText, string sharedSecret) 21 | { 22 | if (string.IsNullOrEmpty(plainText)) 23 | throw new ArgumentNullException("plainText"); 24 | if (string.IsNullOrEmpty(sharedSecret)) 25 | throw new ArgumentNullException("sharedSecret"); 26 | 27 | string outStr = null; // Encrypted string to return 28 | RijndaelManaged aesAlg = null; // RijndaelManaged object used to encrypt the data. 29 | 30 | try 31 | { 32 | // generate the key from the shared secret and the salt 33 | Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(sharedSecret, _salt); 34 | 35 | // Create a RijndaelManaged object 36 | aesAlg = new RijndaelManaged(); 37 | aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8); 38 | 39 | // Create a decryptor to perform the stream transform. 40 | ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); 41 | 42 | // Create the streams used for encryption. 43 | using (MemoryStream msEncrypt = new MemoryStream()) 44 | { 45 | // prepend the IV 46 | msEncrypt.Write(BitConverter.GetBytes(aesAlg.IV.Length), 0, sizeof(int)); 47 | msEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length); 48 | using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) 49 | { 50 | using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) 51 | { 52 | //Write all data to the stream. 53 | swEncrypt.Write(plainText); 54 | } 55 | } 56 | outStr = Convert.ToBase64String(msEncrypt.ToArray()); 57 | } 58 | } 59 | finally 60 | { 61 | // Clear the RijndaelManaged object. 62 | if (aesAlg != null) 63 | aesAlg.Clear(); 64 | } 65 | 66 | // Return the encrypted bytes from the memory stream. 67 | return outStr; 68 | } 69 | 70 | /// 71 | /// Decrypt the given string. Assumes the string was encrypted using 72 | /// EncryptStringAES(), using an identical sharedSecret. 73 | /// 74 | /// The text to decrypt. 75 | /// A password used to generate a key for decryption. 76 | public static string DecryptStringAES(string cipherText, string sharedSecret) 77 | { 78 | if (string.IsNullOrEmpty(cipherText)) 79 | throw new ArgumentNullException("cipherText"); 80 | if (string.IsNullOrEmpty(sharedSecret)) 81 | throw new ArgumentNullException("sharedSecret"); 82 | 83 | // Declare the RijndaelManaged object 84 | // used to decrypt the data. 85 | RijndaelManaged aesAlg = null; 86 | 87 | // Declare the string used to hold 88 | // the decrypted text. 89 | string plaintext = null; 90 | 91 | try 92 | { 93 | // generate the key from the shared secret and the salt 94 | Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(sharedSecret, _salt); 95 | 96 | // Create the streams used for decryption. 97 | byte[] bytes = Convert.FromBase64String(cipherText); 98 | using (MemoryStream msDecrypt = new MemoryStream(bytes)) 99 | { 100 | // Create a RijndaelManaged object 101 | // with the specified key and IV. 102 | aesAlg = new RijndaelManaged(); 103 | aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8); 104 | // Get the initialization vector from the encrypted stream 105 | aesAlg.IV = ReadByteArray(msDecrypt); 106 | // Create a decrytor to perform the stream transform. 107 | ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); 108 | using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) 109 | { 110 | using (StreamReader srDecrypt = new StreamReader(csDecrypt)) 111 | 112 | // Read the decrypted bytes from the decrypting stream 113 | // and place them in a string. 114 | plaintext = srDecrypt.ReadToEnd(); 115 | } 116 | } 117 | } 118 | finally 119 | { 120 | // Clear the RijndaelManaged object. 121 | if (aesAlg != null) 122 | aesAlg.Clear(); 123 | } 124 | 125 | return plaintext; 126 | } 127 | 128 | private static byte[] ReadByteArray(Stream s) 129 | { 130 | byte[] rawLength = new byte[sizeof(int)]; 131 | if (s.Read(rawLength, 0, rawLength.Length) != rawLength.Length) 132 | { 133 | throw new SystemException("Stream did not contain properly formatted byte array"); 134 | } 135 | 136 | byte[] buffer = new byte[BitConverter.ToInt32(rawLength, 0)]; 137 | if (s.Read(buffer, 0, buffer.Length) != buffer.Length) 138 | { 139 | throw new SystemException("Did not read byte array properly"); 140 | } 141 | 142 | return buffer; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Engine.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace BTC.UI5Uploader 11 | { 12 | /// 13 | /// The upload engine. 14 | /// Construct the engine with the path to a eclipse ui5 project folder. 15 | /// If existing, the .Ui5RepositoryUploadParameters is then read to configure the default engine settings. 16 | /// Configure the Settings of the Engine with its properties and call to start the Upload. 17 | /// 18 | class Engine 19 | { 20 | /// 21 | /// Path to the eclipse ui5 project to upload. You can call after assigning a new value to this property to update the engine settings. 22 | /// 23 | public string ProjectDir { get; set; } 24 | /// 25 | /// Relative path to containing the app to package. This folder will become the root of the app. 26 | /// 27 | public string AppSubDir { get; set; } 28 | /// 29 | /// Target name of the UI5 application in the ABAP repository. 30 | /// 31 | public string AppName { get; set; } 32 | /// 33 | /// Description of the UI5 application used for creation in the ABAP repository. 34 | /// 35 | public string AppDescription { get; set; } 36 | /// 37 | /// The package (devclass) to upload the application to. 38 | /// 39 | public string Package { get; set; } 40 | /// 41 | /// The transport request to register the uploaded items to. Can be empty if is "$TMP". 42 | /// 43 | public string TransportRequest { get; set; } 44 | /// 45 | /// Accessinformation and Credentials to acces the SAP system. If is null, a single sign on authentification is performed. 46 | /// 47 | public Credentials Credentials { get; set; } 48 | /// 49 | /// true to enable a test upload. The Upload is simulated an nothing is actually written in the SAP system. 50 | /// 51 | public bool TestMode { get; set; } 52 | /// 53 | /// Enable Delta mode to only register changed files in the . 54 | /// 55 | public bool DeltaMode { get; set; } 56 | /// 57 | /// The external codepage to use in the bsp and mime repostitory. Default is utf-8. 58 | /// 59 | public string ExternalCodepage { get; set; } 60 | /// 61 | /// Timeout for the upload process. 62 | /// 63 | public int Timeout { get; set; } 64 | /// 65 | /// Ignore certificate errors when connecting through https. 66 | /// 67 | public bool IgnoreCertificateErrors { get; set; } 68 | /// 69 | /// List of ignore masks. Each file to upload is tested against the ignore masks. If it matches, it is not uploaded. 70 | /// Either a literal match or a Regex if the string starts with '^' and ends with '$'. Both variants are case sensitive. 71 | /// 72 | public List IgnoreMasks { get; private set; } 73 | 74 | private string cleanedConfigFileContent; 75 | 76 | /// 77 | /// Creates a new instance. Please assign a afterwards. 78 | /// 79 | public Engine() 80 | { 81 | this.IgnoreMasks = new List { "WebContent/META-INF", "WebContent/WEB-INF", ".Ui5RepositoryUploadParameters" }; //Both folders are used by eclipses webserver for lokal testing. .Ui5RepositoryUploadParameters will be zipped separately after some modifications. 82 | this.DeltaMode = true; 83 | this.ExternalCodepage = "utf-8"; 84 | Timeout = 30000; 85 | } 86 | /// 87 | /// Creates a new instace and configures it for the given eclipse ui5 project. 88 | /// 89 | /// Path to the eclipse ui5 project to upload 90 | public Engine(string projectDir) 91 | : this() 92 | { 93 | this.ProjectDir = projectDir; 94 | RetrieveSettingsFromConfig(); 95 | } 96 | /// 97 | /// Reads the .Ui5RepositoryUploadParameters in the and configures this instance with the settings. 98 | /// 99 | public void RetrieveSettingsFromConfig() 100 | { 101 | try 102 | { 103 | if (string.IsNullOrEmpty(ProjectDir)) throw new InvalidOperationException("ProjectDir not set."); 104 | AppName = System.IO.Path.GetFileName(ProjectDir); 105 | Package = "$TMP"; 106 | TransportRequest = string.Empty; 107 | 108 | var configFile = Path.Combine(ProjectDir, ".Ui5RepositoryUploadParameters"); 109 | if (File.Exists(configFile)) 110 | { 111 | var configFileContent = File.ReadAllText(configFile); 112 | configFileContent = System.Text.RegularExpressions.Regex.Replace(configFileContent, @"SAPUI5ApplicationName=(.+)", m => { AppName = m.Groups[1].Value.Trim(); return string.Empty; }); 113 | configFileContent = System.Text.RegularExpressions.Regex.Replace(configFileContent, @"SAPUI5ApplicationPackage=(.+)", m => { Package = m.Groups[1].Value.Trim(); return string.Empty; }); 114 | configFileContent = System.Text.RegularExpressions.Regex.Replace(configFileContent, @"WorkbenchRequest=(.+)", m => { TransportRequest = m.Groups[1].Value.Trim(); return string.Empty; }); 115 | configFileContent = System.Text.RegularExpressions.Regex.Replace(configFileContent, @"SAPUI5ApplicationDescription=(.+)", m => { AppDescription = m.Groups[1].Value.Trim(); return string.Empty; }); 116 | configFileContent = System.Text.RegularExpressions.Regex.Replace(configFileContent, @"ExternalCodePage=(.+)", m => { ExternalCodepage = m.Groups[1].Value.Trim(); return string.Empty; }); 117 | cleanedConfigFileContent = configFileContent; 118 | } 119 | else 120 | { 121 | cleanedConfigFileContent = ""; 122 | Log.Instance.Write("Warning: Configuration File \".Ui5RepositoryUploadParameters\" not found in project folder."); 123 | } 124 | var ignoreFile = Path.Combine(ProjectDir, ".Ui5RepositoryIgnore"); 125 | if (File.Exists(ignoreFile)) 126 | { 127 | IgnoreMasks.AddRange(File.ReadAllLines(ignoreFile).Where(l => !string.IsNullOrWhiteSpace(l))); 128 | } 129 | } 130 | catch (Exception e) 131 | { 132 | Log.Instance.Write(e.ToString()); 133 | } 134 | } 135 | 136 | /// 137 | /// Uploads the UI5 Project int to the SAP system specified by . If is null, a single sign on authentification is performed. 138 | /// 139 | public void Upload() 140 | { 141 | if (string.IsNullOrWhiteSpace(ProjectDir)) throw new InvalidOperationException("ProjectDir not set."); 142 | if (!Directory.Exists(ProjectDir)) throw new InvalidOperationException("ProjectDir not set to a folder."); 143 | if (Credentials == null) throw new InvalidOperationException("Credentials not set."); 144 | if (string.IsNullOrWhiteSpace(Credentials.System)) throw new InvalidOperationException("Credentials.System not set."); 145 | if (string.IsNullOrWhiteSpace(AppName)) throw new InvalidOperationException("AppName not set."); 146 | 147 | if (TestMode) Log.Instance.Write("Test mode on!"); 148 | 149 | try 150 | { 151 | var uri = new Uri(this.Credentials.System); 152 | uri = uri.AddQuery("sap-client", Credentials.Mandant.ToString()); 153 | uri = uri.AddQuery("sapui5applicationname", AppName); 154 | uri = uri.AddQuery("sapui5applicationdescription", AppDescription); 155 | uri = uri.AddQuery("sapui5applicationpackage", Package); 156 | uri = uri.AddQuery("workbenchrequest", TransportRequest); 157 | uri = uri.AddQuery("externalcodepage", ExternalCodepage); 158 | uri = uri.AddQuery("deltamode", DeltaMode ? "true" : "false"); 159 | uri = uri.AddQuery("testmode", TestMode ? "true" : "false"); 160 | 161 | if (IgnoreCertificateErrors) 162 | { 163 | System.Net.ServicePointManager.ServerCertificateValidationCallback += ServerCertificateValidationIgnoreAll; 164 | } 165 | 166 | var cookieContainer = new System.Net.CookieContainer(); 167 | //Try to Authenticate with a HEAD request and retrieve an authentification token cookie before uploading the whole application. 168 | var headRequest = System.Net.HttpWebRequest.Create(uri); 169 | headRequest.PreAuthenticate = false; 170 | headRequest.Timeout = 10000; 171 | headRequest.Method = System.Net.WebRequestMethods.Http.Head; 172 | ((System.Net.HttpWebRequest)headRequest).CookieContainer = cookieContainer; 173 | 174 | if (Credentials.Username != null) 175 | { 176 | headRequest.Credentials = new System.Net.NetworkCredential(Credentials.Username, Credentials.Password); 177 | } 178 | else //SSO 179 | { 180 | headRequest.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation; 181 | headRequest.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials; 182 | } 183 | headRequest.GetResponse().Close(); 184 | 185 | //The actual POST request with the ziped project. 186 | var request = System.Net.HttpWebRequest.Create(uri); 187 | request.Timeout = Timeout * 1000; 188 | request.PreAuthenticate = true; 189 | request.UseDefaultCredentials = false; 190 | ((System.Net.HttpWebRequest)request).CookieContainer = cookieContainer; //Contains the authentification token if acquired 191 | if (Credentials.Username != null) //if not: credentials to reauthenficiate. 192 | { 193 | request.Credentials = new System.Net.NetworkCredential(Credentials.Username, Credentials.Password); 194 | } 195 | else //SSO 196 | { 197 | request.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation; 198 | request.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials; 199 | } 200 | Log.Instance.Write("Uploading project..."); 201 | request.Method = System.Net.WebRequestMethods.Http.Post; 202 | request.ContentType = "application/zip"; 203 | using (var stream = request.GetRequestStream()) 204 | { 205 | using (var zipHelper = new ZipHelper(stream)) 206 | { 207 | zipHelper.Zip(".Ui5RepositoryUploadParameters", Encoding.UTF8.GetBytes(cleanedConfigFileContent)); 208 | zipHelper.Zip(ProjectDir, string.IsNullOrEmpty(AppSubDir)? new[]{ "*" } : new[] { ".Ui5Repository*", AppSubDir }, IgnoreMasks.ToArray()); 209 | } 210 | stream.Close(); 211 | } 212 | Log.Instance.Write("Installing App..."); 213 | var response = (System.Net.HttpWebResponse)request.GetResponse(); 214 | HandleResponse(response); 215 | } 216 | catch (System.Net.WebException ex) 217 | { 218 | if (ex.Response != null) 219 | { 220 | HandleResponse((System.Net.HttpWebResponse)ex.Response); 221 | } 222 | else 223 | { 224 | Log.Instance.Write(ex.ToString()); 225 | Log.Instance.Write("Upload failed!"); 226 | throw new UploadFailedException(); 227 | } 228 | } 229 | } 230 | /// 231 | /// Create a zip file containing the app for manual upload 232 | /// 233 | /// The Filename of the zip file to be created. 234 | public void Bundle(string targetFileName){ 235 | using (var stream = new FileStream(targetFileName, FileMode.Create, FileAccess.Write)) 236 | { 237 | Bundle(stream); 238 | } 239 | } 240 | /// 241 | /// Create a zip containing the app for manual upload. 242 | /// 243 | /// Stream to write the zip file data to. 244 | public void Bundle(Stream stream) 245 | { 246 | var configFile = new StringBuilder(cleanedConfigFileContent); 247 | configFile.AppendLine(); 248 | configFile.AppendLine("SAPUI5ApplicationName=" + AppName); 249 | configFile.AppendLine("SAPUI5ApplicationPackage=" + Package); 250 | configFile.AppendLine("WorkbenchRequest=" + TransportRequest); 251 | configFile.AppendLine("SAPUI5ApplicationDescription=" + AppDescription); 252 | configFile.AppendLine("ExternalCodePage=" + ExternalCodepage); 253 | 254 | using (var zipHelper = new ZipHelper(stream)) 255 | { 256 | zipHelper.Zip(".Ui5RepositoryUploadParameters", Encoding.UTF8.GetBytes(configFile.ToString())); 257 | zipHelper.Zip(ProjectDir, string.IsNullOrEmpty(AppSubDir) ? new[] { "*" } : new[] { ".Ui5Repository*", AppSubDir }, IgnoreMasks.ToArray()); 258 | } 259 | } 260 | 261 | /// 262 | /// Eventhandler for the System.Net.ServicePointManager.ServerCertificateValidationCallback that accepts all certificates. 263 | /// 264 | /// 265 | /// 266 | /// 267 | /// 268 | /// Always true 269 | private bool ServerCertificateValidationIgnoreAll(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certificate, System.Security.Cryptography.X509Certificates.X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors) 270 | { 271 | if (sslPolicyErrors != System.Net.Security.SslPolicyErrors.None) 272 | { 273 | Log.Instance.Write("Certificate has errors. Ignoring."); 274 | } 275 | return true; 276 | } 277 | /// 278 | /// Write something about the response to the Log and raise some exceptions if the upload failed. 279 | /// 280 | /// The WebResponse 281 | private void HandleResponse(System.Net.HttpWebResponse response) 282 | { 283 | using (var reader = new StreamReader(response.GetResponseStream())) 284 | { 285 | Log.Instance.Write("### Begin of response ##########################" + Environment.NewLine + reader.ReadToEnd().Replace("\n", "\r\n")); 286 | Log.Instance.Write("### End of response ##########################"); 287 | } 288 | switch (response.StatusCode) 289 | { 290 | case System.Net.HttpStatusCode.OK: 291 | Log.Instance.Write("Upload successfull!"); 292 | break; 293 | case System.Net.HttpStatusCode.PartialContent: 294 | Log.Instance.Write("Upload finished with warnings."); 295 | break; 296 | case System.Net.HttpStatusCode.Unauthorized: 297 | Log.Instance.Write("Not Authorized!"); 298 | throw new NotAuthorizedException(); 299 | default: 300 | Log.Instance.Write("Upload failed!"); 301 | throw new UploadFailedException(); 302 | } 303 | response.Close(); 304 | } 305 | 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/Log.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace BTC.UI5Uploader 10 | { 11 | /// 12 | /// A simple Log. Singleton. 13 | /// 14 | class Log 15 | { 16 | StringBuilder sb = new StringBuilder(); 17 | private Log() { } 18 | /// 19 | /// The singleton instance 20 | /// 21 | public static readonly Log Instance = new Log(); 22 | /// 23 | /// Returns the text of the log. 24 | /// 25 | /// The text of the log. 26 | public string GetLog() { return sb.ToString(); } 27 | /// 28 | /// Write a new line to the log. 29 | /// 30 | /// The Text to write to the log. 31 | public void Write(string s) 32 | { 33 | s = string.Format("[{1:T}] {0}", s, DateTime.Now); 34 | sb.AppendLine(s); 35 | OnLineAdded(s); 36 | } 37 | /// 38 | /// Write a new line to the log. 39 | /// 40 | /// The formatstring of the line 41 | /// The arguments for the formatstring. 42 | public void Write(string format, params object[] args) 43 | { 44 | Write(string.Format(format, args)); 45 | } 46 | /// 47 | /// EventArgs for a Log event. 48 | /// 49 | public class LogEventArgs: EventArgs{ 50 | /// 51 | /// Creates a new instance 52 | /// 53 | /// The new line of text in the log. 54 | public LogEventArgs(string line){ 55 | this.Line=line; 56 | } 57 | /// 58 | /// The new line of text in the log. 59 | /// 60 | public string Line { get; private set; } 61 | } 62 | /// 63 | /// Event called when a new line of text is added to the log. 64 | /// 65 | public event EventHandler LineAdded; 66 | /// 67 | /// Call the event. 68 | /// 69 | /// The new line of text in the log. 70 | private void OnLineAdded(string line) 71 | { 72 | if (LineAdded != null) LineAdded(this, new LogEventArgs(line)); 73 | } 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/NotAuthorizedException.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace BTC.UI5Uploader 10 | { 11 | /// 12 | /// Exception for a failed authorization. 13 | /// 14 | [Serializable] 15 | public class NotAuthorizedException : Exception 16 | { 17 | public NotAuthorizedException() { } 18 | public NotAuthorizedException(string message) : base(message) { } 19 | public NotAuthorizedException(string message, Exception inner) : base(message, inner) { } 20 | protected NotAuthorizedException( 21 | System.Runtime.Serialization.SerializationInfo info, 22 | System.Runtime.Serialization.StreamingContext context) 23 | : base(info, context) { } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/PackageCommand.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using ManyConsole; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace BTC.UI5Uploader 11 | { 12 | /// 13 | /// Commanline command to create a UI5 application package. 14 | /// The generated Package can be deployed manually to a ABAP repository with the Report /UI5/UI5_REPOSITORY_LOAD_HTTP 15 | /// 16 | class PackageCommand : ConsoleCommand 17 | { 18 | public PackageCommand() 19 | { 20 | DeltaMode = true; 21 | AppSubFolder = "webapp"; 22 | IsCommand("package", oneLineDescription: "Create UI5 Application Package for manual Upload via /UI5/UI5_REPOSITORY_LOAD"); 23 | SkipsCommandSummaryBeforeRunning(); 24 | HasRequiredOption("src|ProjectFolder=", "Root folder of the UI5 Project to upload", o => ProjectFolder = o); 25 | HasOption("app|appfolder=", "Path to the folder containing the application files. Relative to -src. Default: webapp", o => AppSubFolder = o); 26 | HasOption("AppName=", "Target name of the UI5-Application. Extracted from .Ui5RepositoryUploadParameters if option not given.", o => AppName = o); 27 | HasOption("AppDescription=", "Description of the UI5-Application. Needed only when the Application is going to be created. Extracted from .Ui5RepositoryUploadParameters if option not given.", o => AppDescription = o); 28 | HasOption("Package=", "Target Package. Extracted from .Ui5RepositoryUploadParameters if option not given.", o => Package = o); 29 | HasOption("Transport=", "Transport Request. Extracted from .Ui5RepositoryUploadParameters if option not given.", o => TransportRequest = o); 30 | HasOption("disableDeltaMode", "Disable delta mode which adds only changed items to the Transport.", o => DeltaMode = false); 31 | HasAdditionalArguments(1,""); 32 | } 33 | public string ProjectFolder { get; set; } 34 | public string AppSubFolder { get; set; } 35 | public string AppName { get; set; } 36 | public string AppDescription { get; set; } 37 | public string Package { get; set; } 38 | public string TransportRequest { get; set; } 39 | public bool DeltaMode { get; set; } 40 | public int Timeout { get; set; } 41 | 42 | public override int Run(string[] remainingArguments) 43 | { 44 | var engine = new Engine(ProjectFolder); 45 | if (!string.IsNullOrWhiteSpace(AppName)) engine.AppName = AppName; 46 | if (!string.IsNullOrWhiteSpace(AppDescription)) engine.AppDescription = AppDescription; 47 | if (!string.IsNullOrWhiteSpace(Package)) engine.Package = Package; 48 | if (TransportRequest != null) engine.TransportRequest = TransportRequest; 49 | engine.DeltaMode = DeltaMode; 50 | engine.Timeout = Timeout; 51 | engine.AppSubDir = AppSubFolder; 52 | var targetFilename = remainingArguments[0]; 53 | Log.Instance.Write("Project folder: {0}", engine.ProjectDir); 54 | Log.Instance.Write("App name: {0}", engine.AppName); 55 | Log.Instance.Write("Package: {0}", engine.Package); 56 | Log.Instance.Write("Transport: {0}", engine.TransportRequest); 57 | Log.Instance.Write("Ignore Masks: {0}", string.Join(", ", engine.IgnoreMasks)); 58 | 59 | try 60 | { 61 | engine.Bundle(targetFilename); 62 | } 63 | catch (Exception e) 64 | { 65 | Log.Instance.Write(e.ToString()); 66 | return 3; 67 | } 68 | return 0; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using ManyConsole; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using System.Windows.Forms; 10 | 11 | namespace BTC.UI5Uploader 12 | { 13 | static class Program 14 | { 15 | [STAThread] 16 | static int Main(string[] args) 17 | { 18 | Log.Instance.LineAdded += (s, e) => Console.WriteLine(e.Line); 19 | Console.WriteLine("UI5Uploader - Upload Open/SAPUI5 applications to a SAP ABAP repository"); 20 | Console.WriteLine("Copyright (c) 2015 Bernhard Klefer, BTC AG. Read license file for details."); 21 | 22 | var commands = ConsoleCommandDispatcher.FindCommandsInSameAssemblyAs(typeof(Program)); 23 | return ConsoleCommandDispatcher.DispatchCommand(commands, args, Console.Out); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using System.Resources; 5 | 6 | // Allgemeine Informationen über eine Assembly werden über die folgenden 7 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 8 | // die mit einer Assembly verknüpft sind. 9 | [assembly: AssemblyTitle("UI5Uploader")] 10 | [assembly: AssemblyDescription("Upload UI5 applications into a ABAP/BSP repository")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("BTC AG")] 13 | [assembly: AssemblyProduct("UI5Uploader")] 14 | [assembly: AssemblyCopyright("Bernhard Klefer")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | 18 | // Durch Festlegen von ComVisible auf "false" werden die Typen in dieser Assembly unsichtbar 19 | // für COM-Komponenten. Wenn Sie auf einen Typ in dieser Assembly von 20 | // COM zugreifen müssen, legen Sie das ComVisible-Attribut für diesen Typ auf "true" fest. 21 | [assembly: ComVisible(false)] 22 | 23 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 24 | [assembly: Guid("f82ae0ac-0c76-4214-a4b0-ddb9338ecb31")] 25 | 26 | // Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: 27 | // 28 | // Hauptversion 29 | // Nebenversion 30 | // Buildnummer 31 | // Revision 32 | // 33 | // Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern 34 | // übernehmen, indem Sie "*" eingeben: 35 | // [assembly: AssemblyVersion("1.0.*")] 36 | [assembly: AssemblyVersion("1.0.0.0")] 37 | [assembly: AssemblyFileVersion("1.0.0.0")] 38 | [assembly: NeutralResourcesLanguageAttribute("en")] 39 | -------------------------------------------------------------------------------- /src/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Dieser Code wurde von einem Tool generiert. 4 | // Laufzeitversion:4.0.30319.34209 5 | // 6 | // Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn 7 | // der Code erneut generiert wird. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace BTC.UI5Uploader.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. 17 | /// 18 | // Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert 19 | // -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert. 20 | // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen 21 | // mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BTC.UI5Uploader.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle 51 | /// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /src/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Dieser Code wurde von einem Tool generiert. 4 | // Laufzeitversion:4.0.30319.34209 5 | // 6 | // Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn 7 | // der Code erneut generiert wird. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace BTC.UI5Uploader.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.ApplicationScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("%AppData%\\BTC\\SAPUI5AppUpload\\credentials")] 29 | public string CredentialFiles { 30 | get { 31 | return ((string)(this["CredentialFiles"])); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | %AppData%\BTC\SAPUI5AppUpload\credentials 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/SetCredentialsCommand.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using ManyConsole; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace BTC.UI5Uploader 11 | { 12 | /// 13 | /// Command line command to set and save credentials for a SAP system and a user. 14 | /// 15 | class SetCredentialsCommand : ConsoleCommand 16 | { 17 | public SetCredentialsCommand() 18 | { 19 | Mandant = 100; 20 | SkipsCommandSummaryBeforeRunning(); 21 | IsCommand("password", oneLineDescription: "Set and save the credentials for a SAP system"); 22 | HasRequiredOption("s|system=", "SAP System URL", o => System = UriHelper.CreateUri(o).ToString()); 23 | HasOption("m|mandant=", "Mandant (default: 100)", o => Mandant = o == null ? 100 : int.Parse(o)); 24 | HasRequiredOption("u|username=", "Username", o => Username = o); 25 | HasOption("p|password=", "Password. If not supplied you will be prompted for it.", o => Password = o); 26 | HasOption("c|clear", "Clear Password", o => ClearPassword = true); 27 | 28 | } 29 | public string System { get; set; } 30 | public int Mandant { get; set; } 31 | public string Username { get; set; } 32 | public string Password { get; set; } 33 | public bool ClearPassword { get; set; } 34 | 35 | public override int Run(string[] remainingArguments) 36 | { 37 | var store = CredentialStore.Load(); 38 | 39 | if (ClearPassword) 40 | { 41 | var removed = store.RemoveCredentials(System, Mandant, Username); 42 | store.Save(); 43 | if (removed) Log.Instance.Write("Credentials have been removed."); 44 | return 0; 45 | } 46 | 47 | var credentials = new Credentials(System, Mandant, Username); 48 | if (!string.IsNullOrWhiteSpace(Password)) 49 | { 50 | credentials.Password = Password; 51 | store.SetCredentials(credentials); 52 | store.Save(); 53 | return 0; 54 | } 55 | var password1 = GetPasswordFromConsole(string.Format("Enter Password for user {0}: ", Username)); 56 | if (string.IsNullOrEmpty(password1)) 57 | { 58 | Log.Instance.Write("User abort."); 59 | return 5; 60 | } 61 | var password2 = GetPasswordFromConsole("Retype Password: "); 62 | if (password1 != password2) 63 | { 64 | Log.Instance.Write("Password mismatch"); 65 | return 2; 66 | } 67 | credentials.Password = password1; 68 | store.SetCredentials(credentials); 69 | store.Save(); 70 | return 0; 71 | 72 | 73 | } 74 | 75 | public static string GetPasswordFromConsole(string prompt) 76 | { 77 | string password = ""; 78 | try 79 | { 80 | var x = Console.KeyAvailable; 81 | Console.Write(prompt); 82 | ConsoleKeyInfo info = Console.ReadKey(true); 83 | while (info.Key != ConsoleKey.Enter) 84 | { 85 | if (info.Key != ConsoleKey.Backspace) 86 | { 87 | password += info.KeyChar; 88 | info = Console.ReadKey(true); 89 | } 90 | else if (info.Key == ConsoleKey.Backspace) 91 | { 92 | if (!string.IsNullOrEmpty(password)) 93 | { 94 | password = password.Substring 95 | (0, password.Length - 1); 96 | } 97 | info = Console.ReadKey(true); 98 | } 99 | } 100 | Console.WriteLine(); 101 | } 102 | catch (InvalidOperationException) 103 | { 104 | //StdIn is redirected. Running from Eclipse? 105 | Console.WriteLine("WARNING: Console input has been redirected."); 106 | Console.WriteLine(" The Password will be displayed on screen!"); 107 | Console.WriteLine(" You can use the 'UI5Uploader password' command to save your password."); 108 | Console.WriteLine(" Press Enter (empty password) to abort."); 109 | Console.Write(prompt); 110 | password = Console.ReadLine(); 111 | } 112 | return password; 113 | } 114 | 115 | 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/UI5Uploader.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {388848A9-DECD-4FE7-A70D-AC214983DCE4} 8 | Exe 9 | Properties 10 | BTC.UI5Uploader 11 | UI5Uploader 12 | v4.0 13 | 512 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | Cloud_Arrow_Up.ico 37 | 38 | 39 | BTC.UI5Uploader.Program 40 | 41 | 42 | 43 | packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll 44 | 45 | 46 | packages\ManyConsole.0.4.2.17\lib\ManyConsole.dll 47 | 48 | 49 | packages\NDesk.Options.0.2.1\lib\NDesk.Options.dll 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ResXFileCodeGenerator 80 | Resources.Designer.cs 81 | Designer 82 | 83 | 84 | True 85 | Resources.resx 86 | True 87 | 88 | 89 | 90 | SettingsSingleFileGenerator 91 | Settings.Designer.cs 92 | 93 | 94 | True 95 | Settings.settings 96 | True 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 113 | -------------------------------------------------------------------------------- /src/UI5Uploader.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Express 2013 for Windows Desktop 4 | VisualStudioVersion = 12.0.30723.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UI5Uploader", "UI5Uploader.csproj", "{388848A9-DECD-4FE7-A70D-AC214983DCE4}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {388848A9-DECD-4FE7-A70D-AC214983DCE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {388848A9-DECD-4FE7-A70D-AC214983DCE4}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {388848A9-DECD-4FE7-A70D-AC214983DCE4}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {388848A9-DECD-4FE7-A70D-AC214983DCE4}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /src/UploadCommand.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using ManyConsole; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace BTC.UI5Uploader 11 | { 12 | /// 13 | /// Commanline command to upload a UI5 application to a SAP ABAP repository 14 | /// 15 | class UploadCommand : ConsoleCommand 16 | { 17 | public UploadCommand() 18 | { 19 | Mandant = 100; 20 | DeltaMode = true; 21 | Timeout = 30000; 22 | AppSubFolder = "webapp"; 23 | IsCommand("upload", oneLineDescription: "Upload UI5 Application to a SAP Backend"); 24 | SkipsCommandSummaryBeforeRunning(); 25 | HasRequiredOption("s|system=", "SAP System URL", o => System = UriHelper.CreateUri(o).ToString()); 26 | HasOption("m|mandant=", "Mandant (default: 100)", o => Mandant = o == null ? 100 : int.Parse(o)); 27 | HasOption("u|username=", "Username. Omit for Single Sign On.", o => Username = o); 28 | 29 | HasOption("p|password=", "Password. You can set and save the Password with the \"password\" command. If not given and not stored, you will be prompted for it.", o => Password = o); 30 | HasRequiredOption("src|ProjectFolder=", "Root folder of the UI5 Project to upload", o => ProjectFolder = o); 31 | HasOption("app|appfolder=", "Path to the folder containing the application files. Relative to -src. Default webapp", o => AppSubFolder = o); 32 | HasOption("AppName=", "Target name of the UI5-Application. Extracted from .Ui5RepositoryUploadParameters if option not given.", o => AppName = o); 33 | HasOption("AppDescription=", "Description of the UI5-Application. Needed only when the Application is going to be created. Extracted from .Ui5RepositoryUploadParameters if option not given.", o => AppDescription = o); 34 | HasOption("Package=", "Target Package. Extracted from .Ui5RepositoryUploadParameters if option not given.", o => Package = o); 35 | HasOption("Transport=", "Transport Request. Extracted from .Ui5RepositoryUploadParameters if option not given.", o => TransportRequest = o); 36 | HasOption("f|force", "Skip confirmation and upload immediately.", o => SkipConfirmation = true); 37 | HasOption("test", "Test run. The installation will be simulated.", o => TestMode = true); 38 | HasOption("disableDeltaMode", "Disable delta mode which adds only changed items to the Transport.", o => DeltaMode = false); 39 | HasOption("ignoreCertificateErrors", "Ignore SSL certificate errors when connecting via https.", o => IgnoreCertificateErrors = true); 40 | HasOption("timeout=", "Time to wait for the upload and installation of the app in seconds. Default 30000", o => { if (o != null) Timeout = int.Parse(o); }); 41 | } 42 | public string System { get; set; } 43 | public int Mandant { get; set; } 44 | public string Username { get; set; } 45 | public string Password { get; set; } 46 | public string ProjectFolder { get; set; } 47 | public string AppSubFolder { get; set; } 48 | public string AppName { get; set; } 49 | public string AppDescription { get; set; } 50 | public string Package { get; set; } 51 | public string TransportRequest { get; set; } 52 | public bool SkipConfirmation { get; set; } 53 | public bool TestMode { get; set; } 54 | public bool DeltaMode { get; set; } 55 | public int Timeout { get; set; } 56 | public bool IgnoreCertificateErrors { get; set; } 57 | 58 | public override int Run(string[] remainingArguments) 59 | { 60 | var engine = new Engine(ProjectFolder); 61 | if (!string.IsNullOrWhiteSpace(AppName)) engine.AppName = AppName; 62 | if (!string.IsNullOrWhiteSpace(AppDescription)) engine.AppDescription = AppDescription; 63 | if (!string.IsNullOrWhiteSpace(Package)) engine.Package = Package; 64 | if (TransportRequest != null) engine.TransportRequest = TransportRequest; 65 | engine.DeltaMode = DeltaMode; 66 | engine.TestMode = TestMode; 67 | engine.Timeout = Timeout; 68 | engine.AppSubDir = AppSubFolder; 69 | engine.IgnoreCertificateErrors = IgnoreCertificateErrors; 70 | Credentials credentials = null; 71 | if (!string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password)) 72 | { 73 | credentials = new Credentials(System, Mandant, Username); 74 | credentials.Password = Password; 75 | } 76 | else if (!string.IsNullOrWhiteSpace(Username)) 77 | { 78 | var credentialStore = CredentialStore.Load(); 79 | credentials = credentialStore.GetCredentials(System, Mandant, Username); 80 | if (credentials == null) 81 | { 82 | credentials = new Credentials(System, Mandant, Username); 83 | credentials.Password = SetCredentialsCommand.GetPasswordFromConsole(string.Format("Enter Password for user {0}: ", Username)); 84 | if (string.IsNullOrEmpty(credentials.Password)) { 85 | Log.Instance.Write("User abort."); 86 | return 5; 87 | } 88 | Console.Write("Save Password? [Y/N] "); 89 | var answer = Console.ReadLine(); 90 | if (answer == null) 91 | { 92 | Log.Instance.Write("Non interactive console. Aborting."); 93 | return 6; 94 | } 95 | if (answer.Trim().Equals("Y", StringComparison.OrdinalIgnoreCase)) 96 | { 97 | credentialStore.SetCredentials(credentials); 98 | credentialStore.Save(); 99 | } 100 | 101 | } 102 | } 103 | else //SSO 104 | { 105 | credentials = new Credentials(System, Mandant, null); 106 | } 107 | engine.Credentials = credentials; 108 | Log.Instance.Write("System: {0}", engine.Credentials.System); 109 | Log.Instance.Write("Mandant: {0}", engine.Credentials.Mandant); 110 | Log.Instance.Write("Username: {0}", engine.Credentials.Username ?? "SSO"); 111 | Log.Instance.Write("Project folder: {0}", engine.ProjectDir); 112 | Log.Instance.Write("App name: {0}", engine.AppName); 113 | Log.Instance.Write("Package: {0}", engine.Package); 114 | Log.Instance.Write("Transport: {0}", engine.TransportRequest); 115 | Log.Instance.Write("Ignore Masks: {0}", string.Join(", ", engine.IgnoreMasks)); 116 | 117 | if (!SkipConfirmation) 118 | { 119 | Console.WriteLine("Do you want to upload? [Y/N]"); 120 | var answer = Console.ReadLine(); 121 | if (answer == null) 122 | { 123 | Log.Instance.Write("Non interactive console. Use -f to disable this prompt."); 124 | return 6; 125 | } 126 | if (!answer.Trim().Equals("Y", StringComparison.OrdinalIgnoreCase)) 127 | { 128 | Log.Instance.Write("User abort."); 129 | return 0; 130 | } 131 | } 132 | try 133 | { 134 | engine.Upload(); 135 | } 136 | catch (UploadFailedException) 137 | { 138 | return 2; 139 | } 140 | catch (NotAuthorizedException) 141 | { 142 | var credentialStore = CredentialStore.Load(); 143 | credentials = credentialStore.GetCredentials(System, Mandant, Username); 144 | if (credentials != null) 145 | { 146 | Log.Instance.Write("Removing saved password."); 147 | credentialStore.RemoveCredentials(System, Mandant, Username); 148 | credentialStore.Save(); 149 | } 150 | return 1; 151 | 152 | } 153 | catch (Exception e) 154 | { 155 | Log.Instance.Write(e.ToString()); 156 | return 3; 157 | } 158 | return 0; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/UploadFailedException.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace BTC.UI5Uploader 10 | { 11 | /// 12 | /// Exception for a failed upload. 13 | /// 14 | [Serializable] 15 | public class UploadFailedException : Exception 16 | { 17 | public UploadFailedException() { } 18 | public UploadFailedException(string message) : base(message) { } 19 | public UploadFailedException(string message, Exception inner) : base(message, inner) { } 20 | protected UploadFailedException( 21 | System.Runtime.Serialization.SerializationInfo info, 22 | System.Runtime.Serialization.StreamingContext context) 23 | : base(info, context) { } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/UriHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Web; 9 | 10 | namespace BTC.UI5Uploader 11 | { 12 | /// 13 | /// Helper class with extension methods and helper methods to build URIs 14 | /// 15 | static class UriHelper 16 | { 17 | /// 18 | /// Adds the name-value-pair to the query. 19 | /// 20 | /// The Uri to add the query parameter to 21 | /// Name of the query parameter 22 | /// Value of the query parameter 23 | /// The Uri including the new query parameter 24 | public static Uri AddQuery(this Uri uri, string name, string value) 25 | { 26 | var ub = new UriBuilder(uri); 27 | 28 | // decodes urlencoded pairs from uri.Query to HttpValueCollection 29 | var httpValueCollection = HttpUtility.ParseQueryString(uri.Query); 30 | httpValueCollection.Add(name, value); 31 | 32 | // this code block is taken from httpValueCollection.ToString() method 33 | // and modified so it encodes strings with HttpUtility.UrlEncode 34 | if (httpValueCollection.Count == 0) 35 | ub.Query = String.Empty; 36 | else 37 | { 38 | var sb = new StringBuilder(); 39 | 40 | for (int i = 0; i < httpValueCollection.Count; i++) 41 | { 42 | string text = httpValueCollection.GetKey(i); 43 | { 44 | text = HttpUtility.UrlEncode(text); 45 | 46 | string val = (text != null) ? (text + "=") : string.Empty; 47 | string[] vals = httpValueCollection.GetValues(i); 48 | 49 | if (sb.Length > 0) 50 | sb.Append('&'); 51 | 52 | if (vals == null || vals.Length == 0) 53 | sb.Append(val); 54 | else 55 | { 56 | if (vals.Length == 1) 57 | { 58 | sb.Append(val); 59 | sb.Append(HttpUtility.UrlEncode(vals[0])); 60 | } 61 | else 62 | { 63 | for (int j = 0; j < vals.Length; j++) 64 | { 65 | if (j > 0) 66 | sb.Append('&'); 67 | 68 | sb.Append(val); 69 | sb.Append(HttpUtility.UrlEncode(vals[j])); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | ub.Query = sb.ToString(); 77 | } 78 | 79 | return ub.Uri; 80 | } 81 | 82 | /// 83 | /// Create a valid URI to the ui5 upload service if the user has given only the host (and port). 84 | /// The scheme is set to http if missing and the path is set to /sap/bc/ui5upload. 85 | /// 86 | /// the uri to the sap upload service as given by the user. 87 | /// 88 | public static Uri CreateUri(string system) 89 | { 90 | if (!System.Text.RegularExpressions.Regex.IsMatch(system, @"^\w+://")) system = "http://" + system; 91 | var builder = new UriBuilder(system); 92 | if (builder.Path == "/") builder.Path = "/sap/bc/ui5upload"; 93 | return builder.Uri; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ZipHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright Bernhard Klefer (BTC AG, Escherweg 5, 26121 Oldenburg, Germany) 2 | // This file is part of the BTC.UI5Uploader 3 | // BTC.UI5Uploader ist distributed under the BSD 2-clause license (full text see file license). 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace BTC.UI5Uploader 11 | { 12 | /// 13 | /// Helper-Class for recursive zipping of a folder. 14 | /// 15 | class ZipHelper : IDisposable 16 | { 17 | ICSharpCode.SharpZipLib.Zip.ZipOutputStream zipStream; 18 | /// 19 | /// Creates a new Instance that will write to the specified stream 20 | /// 21 | /// Stream to write the zipped data to 22 | public ZipHelper(Stream stream) 23 | { 24 | zipStream = new ICSharpCode.SharpZipLib.Zip.ZipOutputStream(stream); 25 | zipStream.SetLevel(9); 26 | zipStream.UseZip64 = ICSharpCode.SharpZipLib.Zip.UseZip64.Off; 27 | 28 | } 29 | /// 30 | /// Zips the specified . 31 | /// 32 | /// The folder to zip 33 | /// 34 | /// the files and directories within the to include in the zip. 35 | /// These names are only matched in the but not recursive. 36 | /// You can use the default globbing (* and ?). 37 | /// 38 | /// Names of the files or directories to exclude from zip. Full case sensitive match. No globbing. Directories have a trailing "/". 39 | public void Zip(string baseFolder, string[] filesToZip, string[] filesToIgnore) 40 | { 41 | var baseDir = new DirectoryInfo(baseFolder); 42 | foreach (var fileMask in filesToZip) 43 | { 44 | foreach (var file in baseDir.GetFiles(fileMask, SearchOption.TopDirectoryOnly)) 45 | { 46 | if (IgnoreFile(baseDir.FullName, file.FullName, filesToIgnore)) continue; 47 | var zipEntry = new ICSharpCode.SharpZipLib.Zip.ZipEntry(file.Name); 48 | zipStream.PutNextEntry(zipEntry); 49 | using (var fileStream = file.OpenRead()) fileStream.CopyTo(zipStream); 50 | } 51 | foreach (var subDir in baseDir.GetDirectories(fileMask, SearchOption.TopDirectoryOnly)) 52 | { 53 | if (IgnoreFile(baseDir.FullName, subDir.FullName, filesToIgnore)) continue; 54 | ZipDirectory(zipStream, baseDir, subDir, filesToIgnore); 55 | } 56 | } 57 | } 58 | /// 59 | /// Zips the specified . 60 | /// 61 | /// Relative filename in zip. 62 | /// The content of the file 63 | public void Zip(string fileName, Stream contentStream) 64 | { 65 | var nameTransform = new ICSharpCode.SharpZipLib.Zip.ZipNameTransform(); 66 | var relativeName = nameTransform.TransformFile(fileName); 67 | var zipEntry = new ICSharpCode.SharpZipLib.Zip.ZipEntry(relativeName); 68 | zipStream.PutNextEntry(zipEntry); 69 | contentStream.CopyTo(zipStream); 70 | } 71 | /// 72 | /// Zips the specified . 73 | /// 74 | /// Relative filename in zip. 75 | /// The content of the file 76 | public void Zip(string fileName, byte[] content) 77 | { 78 | if (content==null) return; 79 | var nameTransform = new ICSharpCode.SharpZipLib.Zip.ZipNameTransform(); 80 | var relativeName = nameTransform.TransformFile(fileName); 81 | var zipEntry = new ICSharpCode.SharpZipLib.Zip.ZipEntry(relativeName); 82 | zipStream.PutNextEntry(zipEntry); 83 | zipStream.Write(content, 0, content.Length); 84 | } 85 | /// 86 | /// Zips the specified to the . 87 | /// 88 | /// Stream to write the compressed data to. 89 | /// The folder to zip 90 | /// 91 | /// the files and directories within the to include in the zip. 92 | /// These names are only matched in the but not recursive. 93 | /// You can use the default globbing (* and ?). 94 | /// 95 | /// Names of the files or directories to exclude from zip. Full case sensitive match. No globbing. Directories have a trailing "/". 96 | public static void Zip(Stream stream, string baseFolder, string[] filesToZip, string[] filesToIgnore) 97 | { 98 | using (var s = new ICSharpCode.SharpZipLib.Zip.ZipOutputStream(stream)) 99 | { 100 | s.SetLevel(9); 101 | s.UseZip64 = ICSharpCode.SharpZipLib.Zip.UseZip64.Off; 102 | var baseDir = new DirectoryInfo(baseFolder); 103 | foreach (var fileMask in filesToZip) 104 | { 105 | foreach (var file in baseDir.GetFiles(fileMask, SearchOption.TopDirectoryOnly)) 106 | { 107 | if (IgnoreFile(baseDir.FullName, file.FullName, filesToIgnore)) continue; 108 | var zipEntry = new ICSharpCode.SharpZipLib.Zip.ZipEntry(file.Name); 109 | s.PutNextEntry(zipEntry); 110 | using (var fileStream = file.OpenRead()) fileStream.CopyTo(s); 111 | } 112 | foreach (var subDir in baseDir.GetDirectories(fileMask, SearchOption.TopDirectoryOnly)) 113 | { 114 | if (IgnoreFile(baseDir.FullName, subDir.FullName, filesToIgnore)) continue; 115 | ZipDirectory(s, baseDir, subDir, filesToIgnore); 116 | } 117 | 118 | } 119 | s.Finish(); 120 | } 121 | } 122 | /// 123 | /// Recursive Zipping of a directory. 124 | /// 125 | /// 126 | /// 127 | /// 128 | /// 129 | private static void ZipDirectory(ICSharpCode.SharpZipLib.Zip.ZipOutputStream zipStream, DirectoryInfo baseDir, DirectoryInfo dir, string[] filesToIgnore) 130 | { 131 | var nameTransform = new ICSharpCode.SharpZipLib.Zip.ZipNameTransform(baseDir.FullName); 132 | foreach (var file in dir.GetFiles()) 133 | { 134 | if (IgnoreFile(baseDir.FullName, file.FullName, filesToIgnore)) continue; 135 | var relativeName = nameTransform.TransformFile(file.FullName); 136 | var zipEntry = new ICSharpCode.SharpZipLib.Zip.ZipEntry(relativeName); 137 | zipStream.PutNextEntry(zipEntry); 138 | using (var fileStream = file.OpenRead()) fileStream.CopyTo(zipStream); 139 | } 140 | foreach (var subDir in dir.GetDirectories()) 141 | { 142 | if (IgnoreFile(baseDir.FullName, subDir.FullName, filesToIgnore)) continue; 143 | ZipDirectory(zipStream, baseDir, subDir, filesToIgnore); 144 | } 145 | } 146 | /// 147 | /// Test if file is matched by an ignore mask. 148 | /// 149 | /// basefolder for relative filenames in ignore masks 150 | /// full filename 151 | /// list of ignore masks. Either a literal match or a Regex if the string starts with '^' and ends with '$'. Both variants are case sensitive. 152 | /// 153 | private static bool IgnoreFile(string baseFolder, string fullFilename, IEnumerable ignoreMasks) 154 | { 155 | var relativeFilename = fullFilename; 156 | if (!baseFolder.EndsWith("\\")) baseFolder += "\\"; 157 | if (fullFilename.StartsWith(baseFolder, StringComparison.OrdinalIgnoreCase)) 158 | { 159 | relativeFilename = fullFilename.Substring(baseFolder.Length); 160 | } 161 | relativeFilename = relativeFilename.Replace('\\', '/'); 162 | foreach (var mask in ignoreMasks) 163 | { 164 | if (mask.StartsWith("^") && mask.EndsWith("$")) //Regex match 165 | { 166 | try 167 | { 168 | if (System.Text.RegularExpressions.Regex.IsMatch(relativeFilename, mask)) return true; 169 | } 170 | catch 171 | { 172 | //Invalid Regex. 173 | } 174 | } 175 | else //Literal match 176 | { 177 | if (relativeFilename.Equals(mask, StringComparison.Ordinal)) return true; 178 | } 179 | } 180 | return false; 181 | } 182 | 183 | public void Dispose() 184 | { 185 | Dispose(true); 186 | GC.SuppressFinalize(this); 187 | } 188 | 189 | protected virtual void Dispose(bool disposing) 190 | { 191 | if (disposing) 192 | { 193 | if (zipStream != null) 194 | { 195 | zipStream.Finish(); 196 | zipStream.Dispose(); 197 | } 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /z_btc_ui5_upload_webservice.abap: -------------------------------------------------------------------------------- 1 | "! HTTP Handler for the UI5Upload Webservice. Installs the UI5 application contained in a ZIP file uploaded in the body of the HTTP request. 2 | "! Some form parameters can be given to control the upload behaviour. Parameters that are given overwrite the corresponding settings in the .Ui5RepositoryUploadParameters. 3 | "! 4 | "! Parameters: 5 | "! sapui5applicationname: Name of the Application. 6 | "! sapui5applicationdescription: Description of the Application. Used only when the application is created. 7 | "! sapui5applicationpackage: Packaged to deploy the application to. 8 | "! workbenchrequest: Transport request to deploy the application with. can be omitted if the sapui5applicationpackage is "$TMP". 9 | "! externalcodepage: You should use "utf-8" here. 10 | "! acceptunixstyleeol: <true|false> Triggers automatic conversion of unix style text files to the Windows format expected by the repository. 11 | "! deltamode: <true|false> Only changes to current state in the repository get registered in workbench request. 12 | "! testmode: <true|false> Triggers a test run in which no upload takes place. The HTTP response holds a detailed log then. 13 | CLASS z_btc_ui5_upload_webservice DEFINITION 14 | PUBLIC FINAL CREATE PUBLIC . 15 | PUBLIC SECTION. 16 | INTERFACES if_http_extension. 17 | PROTECTED SECTION. 18 | PRIVATE SECTION. 19 | "! TTL of the ZIP-Data in the HTTP cache in seconds. 20 | CONSTANTS zip_cache_ttl_s TYPE i VALUE 180. "3 minutes 21 | DATA url_parameters TYPE tihttpnvp. 22 | 23 | "! Uploads the ZIPs binary data to the http cache with a timeout of zip_cache_livetime_s seconds and returns the URL to the cached ZIP. 24 | METHODS upload_zip_to_http_cache 25 | IMPORTING i_zip_data TYPE xstring 26 | RETURNING VALUE(r_url) TYPE string. 27 | 28 | "! Returns the value of the URL parameter specified by i_parameter_name 29 | "! @parameter i_parameter_name | Name of the URL parameter to retrieve 30 | "! @parameter r_result | The value of the URL parameter or INITIAL if the URL Parameter is not existent. 31 | METHODS get_url_parameter 32 | IMPORTING i_parameter_name TYPE string 33 | RETURNING VALUE(r_result) TYPE string. 34 | ENDCLASS. 35 | 36 | 37 | 38 | CLASS z_btc_ui5_upload_webservice IMPLEMENTATION. 39 | METHOD if_http_extension~handle_request. 40 | if server->request->get_method( ) = 'HEAD'. 41 | server->response->set_status( code = 200 reason = 'OK' ). 42 | return. 43 | endif. 44 | if server->request->get_method( ) <> 'POST'. 45 | server->response->set_status( code = 500 reason = 'Only POST supported.' ). 46 | return. 47 | endif. 48 | server->request->get_form_fields( CHANGING fields = url_parameters ). 49 | data(zip_url) = upload_zip_to_http_cache( server->request->get_data( ) ). 50 | 51 | DATA success TYPE char1. 52 | DATA log_messages TYPE string_table. 53 | try. 54 | CALL FUNCTION '/UI5/UI5_REPOSITORY_LOAD_HTTP' 55 | EXPORTING 56 | iv_url = zip_url 57 | iv_sapui5_application_name = get_url_parameter( `sapui5applicationname` ) 58 | iv_sapui5_application_desc = get_url_parameter( `sapui5applicationdescription` ) 59 | iv_package = conv devclass( get_url_parameter( `sapui5applicationpackage` ) ) 60 | iv_workbench_request = conv trkorr( get_url_parameter( `workbenchrequest` ) ) 61 | iv_external_code_page = get_url_parameter( `externalcodepage` ) 62 | iv_accept_unix_style_eol = switch #( get_url_parameter( `acceptunixstyleeol` ) when `true` then abap_true when `false` then abap_false else abap_undefined ) 63 | iv_delta_mode = switch #( get_url_parameter( `deltamode` ) when `true` then abap_true when `false` then abap_false else abap_undefined ) 64 | iv_test_mode = switch #( get_url_parameter( `testmode` ) when `true` then abap_true else abap_false ) 65 | IMPORTING 66 | ev_success = success 67 | ev_log_messages = log_messages. 68 | catch cx_root into data(e). 69 | success = 'E'. 70 | while e is bound. 71 | e->get_source_position( importing program_name = data(program) include_name = data(include) source_line = data(line) ). 72 | insert |{ cl_abap_classdescr=>get_class_name( e ) }: { e->get_text( ) } in { include } { program } line { line }| into table log_messages. 73 | e = e->previous. 74 | endwhile. 75 | endtry. 76 | case success. 77 | when 'E'. 78 | server->response->set_status( code = 500 reason = 'Installation of UI5 application failed.' ). 79 | when 'S'. 80 | server->response->set_status( code = 200 reason = 'Finished' ). 81 | when others. 82 | server->response->set_status( code = 206 reason = 'Finished with warnings' ). 83 | endcase. 84 | 85 | CONCATENATE LINES OF log_messages INTO data(log_string) SEPARATED BY cl_abap_char_utilities=>newline. 86 | server->response->set_cdata( data = log_string ). 87 | ENDMETHOD. 88 | 89 | METHOD get_url_parameter. 90 | IF line_exists( url_parameters[ name = i_parameter_name ] ). 91 | r_result = url_parameters[ name = i_parameter_name ]-value. 92 | ENDIF. 93 | ENDMETHOD. 94 | 95 | METHOD upload_zip_to_http_cache. 96 | DATA(cached_response) = NEW cl_http_response( add_c_msg = 1 ). 97 | cached_response->set_content_type( `application/zip` ). 98 | cached_response->set_data( i_zip_data ). 99 | cached_response->if_http_response~set_status( code = 200 reason = 'OK' ). 100 | cached_response->if_http_response~server_cache_expire_rel( zip_cache_ttl_s ). 101 | cl_wd_utilities=>construct_wd_url( EXPORTING application_name = `UI5Upload` IMPORTING out_absolute_url = r_url ). 102 | 103 | r_url = r_url && `/` && cl_system_uuid=>create_uuid_c32_static( ). 104 | cl_http_server=>server_cache_upload( response = cached_response url = r_url ). 105 | ENDMETHOD. 106 | 107 | ENDCLASS. --------------------------------------------------------------------------------