├── .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 | 
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 | 
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.
--------------------------------------------------------------------------------