SSIS CSV Source
PreviousNext

SSIS CSV Source Adapter can be used to parse / extract local CSV file, Web URL CSV or direct CSV String (coming from variable). Component also supports many advanced options not found in Native Flat File Source.

Download SSIS PowerPack

Content

Video Tutorial

Step-By-Step

In this section you will learn how to use CSV Source Connector to extract data from CSV file (In this case its Local file URL).
  1. Firstly, You need to Download and Install SSIS ZappySys PowerPack.
  2. Once you finished first step, Open Visual Studio and Create New SSIS Package Project.
  3. Now, Drag and Drop SSIS Data Flow Task from SSIS Toolbox.
    SSIS Data Flow Task - Drag and Drop
  4. Double click on the DataFlow task to see DataFlow designer surface.
  5. Here, In Visual Studio, drag and drop the ZS CSV Source (Web API or File) in the design panel
    SSIS CSV Source (Web API or File) - Drag and Drop
  6. Double click on ZS CSV Source (Web API or File) Configure it.
  7. While configure this task you need to do below settings.
    Example: Here is Example of Local or Server Path.
    
    c:\data\cust-1.csv (Local file Path)
    http://www.zappysys.com/downloads/files/test/cust-1.csv (Server file Path)
    
    Note: If you want to operation with multiple files then use wild card pattern as below (when you use wild card pattern in source path then system will treat target path as folder regardless you end with slash).
    
    c:\data\cust-1*.csv (all files starting with file name)
    c:\data\subfolder\*.csv (all files with .csv Extension and located under folder subfolder)
    
    You can also create Variable for file name and use it in CSV source
    SSIS CSV Source (Web API or File) - Variable
    
    SSIS CSV Source (Web API or File) - Configure
  8. We have Advanced Tab for setting it.
    SSIS CSV Source (Web API or File) - Configure
  9. Click OK to save ZS CSV Source (Web API or File) UI settings.
  10. You can Select Direct Value or Variable as CSV Source Type.
  11. Now, Just Drag and Drop Our Free ZS Trash Destination from SSIS Toolbox.
    SSIS Trash Destination - Drag and Drop
  12. And than connect ZS CSV Source (Web API or File) to Trash Destination.
  13. Lets Double click on ZS Trash Destination to Configure it.
    SSIS Trash Destination - Drag and Drop
  14. Thats all, Just Execute or run your Package.
    SSIS CSV Source (Web API or File) - Execute Task

How to read data from CSV file URL with zip format.

  1. Double click on CSV Source (Web API or File) to configure it.
  2. Set AccessMode to File path or web URL. Enter following URl.
    https://zappysys.com/downloads/files/test/invoices.csv.zip
    https://zappysys.com/downloads/files/test/invoices.csv.gz
    
    In the settings tab set HTTP Request Method to GET. Check on First row has column name.
    SSIS CSV Source (Web API or File) - Configure
  3. Now, in the compression tab, set file compression format to Zip(for only .zip file) or GZip(for only .gz file).
    SSIS CSV Source (Web API or File) - Compressed Tab
  4. Click on Preview button to see Data Preview.
  5. Click on OK button to save CSV Source configure setting UI.
  6. Now, Just Drag and Drop Our Free ZS Trash Destination from SSIS Toolbox.
    SSIS Trash Destination - Drag and Drop
  7. And than connect ZS CSV Source (Web API or File) to Trash Destination.
  8. Lets Double click on ZS Trash Destination to Configure it.
    SSIS Trash Destination - Drag and Drop
  9. Thats all, Just Execute or run your Package.
    SSIS CSV Source (Web API or File) - Execute Task

Properties

Property Name Description
LoggingMode LoggingMode determines how much information is logged during Package Execution. Set Logging mode to Debugging for maximum log.

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
Normal [0] Normal
Medium [1] Medium
Detailed [2] Detailed
Debugging [3] Debugging
PrefixTimestamp When you enable this property it will prefix timestamp before Log messages.
TreatBlankNumberAsNull Treat empty string as NULL for any numeric data types
TreatBlankBoolAsNull Treat empty string as NULL for bool data types
TreatBlankDateAsNull Treat empty string as NULL for any date/time data types
ColumnDelimiter Column delimiter for data you like to parse. To use custom delimiter enter 4-digit hex string starting with \x (e.g. you can enter \x0009 for Tab character). For multiple characters repeat group. e.g. \x00090009 if you need two tabs.
HasColumnHeaderRow Column delimiter for data you like to parse.
ThrowErrorOnColumnCountMismatch Throw error if record has different number of columns than actual columns detected based on first row
ThrowErrorOnNoRecordFound Throw error if no record found or file is blank
AllowComment Allow comment lines which can be skipped by parser. When comment line found row is skipped. See CommentCharacter to configure first character for commented line.
CommentCharacter Allow lines with comment. When comment line found row is skipped. See LineCommentCharacter property to configure first character for commented line.
SkipRows Total data rows you like to skip (after header row)
SkipHeaderCommentRows Total rows you like to skip before header row. If its header less file then skip initial N rows (before any data row).
IgnoreBlankLines When this option is enabled, blank lines are skipped.
SkipEmptyRecords When this option is enabled, any row with empty values in all fields is skipped (e.g. , , , , ).
TrimHeaders Trim column names if whitespace found before or after name
TrimFields Trim value for each field if whitespace found before or after
IgnoreQuotes Ignore quote character and consider it part of actual value
QuoteCharacter Quote character for quoted values.
Encoding Encoding of source file

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
Default [0] Default
ASCII [1] ASCII
UTF8 [2] UTF-8
UTF16 [3] UTF-16 LE (i.e. Unicode Little Endian)
UTF32 [4] UTF-32
UTF8WithoutBOM [5] UTF-8 Without BOM
UTF32WithoutBOM [6] UTF-32 Without BOM
UTF7 [7] UTF-7
UTF7WithoutBOM [8] UTF-7 Without BOM
UTF16WithoutBOM [9] UTF-16 Without BOM
BigEndian [10] UTF-16 BE (i.e. Unicode Big Endian)
BigEndianWithoutBOM [11] UTF-16 BE Without BOM
CharacterSet Character set for text (e.g. windows-1250 )
Culture Culture code (e.g. pt-BT). This helps to parse culture specific number formats (e.g. In some culture you may have comma rather than decimal points 0.1 can be 0,1)
AccessMode Defines how to read the JSON file or direct string

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
DirectValue [0] Direct value
ValueFromVariable [1] Direct value from variable
DirectPath [2] File path or web URL
PathFromVariable [3] File path or web URL from variable
DirectValue Defines how to read the JSON file or direct string
ValueVariable Variable name which holds JSON string
PathVariable Variable name which holds data file path or url
DirectPath JSON file path (e.g. c:\data\myfile.json) or pattern to process multiple files (e.g. c:\data\*.json)
Recursive Include files from sub folders too.
ContinueOnFileNotFoundError By default process stops with error if specified local file is not found. Set this property to true if you wish to continue rather than throwing file not found error.
HttpHeaders Set this if you want to set custom Http headers. You may use variable anywhere in the header value using syntax {{User::YourVarName}}. Syntax of Header key value pair is : <request><header><name>x-myheader-1</name><value>AAA</value></header> <header><name>x-myheader-2</name><value>BBB</value></header></request>
HttpRequestData User defined data you wish to send along with your HTTP Request (e.g. Upload file data, Form POST data). Usually you have to set content-type of your data but if you select RequestMethod=POST then system will automatically set content-type=application/x-www-form-urlencoded.
HttpRequestMethod Http Web Request Method (e.g. POST, GET, PUT, LIST, DELETE...). Refer your API documentation if you are not sure which method you have to use.
HttpRequestContentType Specifies content type for data you wish to POST. If you select Default option then system default content type will be used (i.e. application/x-www-form-urlencoded). If you specify Content-Type header along with this option then header value takes precedence.

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
Default [0] Default
TextPlain [1] Text (text/plain)
ApplicationJson [2] JSON (application/json)
ApplicationXml [3] XML (application/xml)
TextXml [4] XML (text/xml)
TextXmlUtf8 [5] XML (text/xml;charset=UTF-8)
TextHtml [6] HTML (text/html)
ApplicationFormUrlencoded [7] Form (application/x-www-form-urlencoded)
Raw [8] Raw (No content-type)
IsMultiPartUpload Check this option if you want to upload file(s) (i.e. POST RAW file data) or send data using Multi-Part encoding method (i.e. Content-Type: multipart/form-data). Multi-Part request allows you to mix key/value and upload files in same request. On the other hand raw upload allows only single file upload (without any key/value)

==== Raw Upload (Content-Type: application/octet-stream) =====  
To upload single file in raw mode check this option and specify full file path starting with @ sign in the Body (e.g.  @c:\data\myfile.zip )

==== Form-Data / Multipart Upload (Content-Type: multipart/form-data) =====  
To treat your Request data as multi part fields you must specify key/value pairs separated by new lines into RequestData field (i.e. Body). Each key value pair is entered on new-line and key/value are separated using equal sign (=). Preceding and trailing spaces are ignored also blank lines are ignored.
If field value has some any special character(s) then use escape sequence (e.g. For NewLine: \r\n, For Tab: \t, For at (@): \@). When value of any field starts with at sign (@) its automatically treated as File you want to upload. By default file content type is determined based on extension however you can supply content type manually for any field using this way [ YourFileFieldName.Content-Type=some-content-type ]. By default File Upload Field always includes Content-Type in the request (non file fields do not have content-type by default unless you supply manually). For some reason if you dont want to use Content-Type header in your request then supply blank Content-Type to exclude this header altogather [e.g. SomeFieldName.Content-Type= ]. In below example we have supplied Content-Type for file2 and SomeField1, all other fields are using default content-type.
See below Example of uploading multiple files along with additional fields.

file1=@c:\data\Myfile1.txt
file2=@c:\data\Myfile2.json
file2.Content-Type=application/json
SomeField1=aaaaaaa
SomeField1.Content-Type=text/plain
SomeField2=12345
SomeFieldWithNewLineAndTab=This is line1\r\nThis is line2\r\nThis is \ttab \ttab \ttab
SomeFieldStartingWithAtSign=\@MyTwitterHandle
UseProxy Enable custom proxy settings (If this is not set then system default proxy will be used. To disable proxy totally uncheck this option and check DoNotUseDefaultProxy option if available)
ProxyUrl Web URL of Proxy server (including port  if necessary). [e.g. http://myproxyserver:8080/]
UseProxyCreds Enable passing userid and password to proxy server
ProxyUserName Proxy username
ProxyPassword Proxy password
NextUrlAttribute If Service response support pagination using some sort of next url attribute then specify which attribute name in JSON Response string which holds next url. If no attribute found or its null then component will stop fetching next resultset. Example: $.pagingInfo.nextUrl
PrevUrlAttribute If Service response support pagination using some sort of prev/next url attribute then specify which previous link attribute name from JSON Response string which holds previous url.
NextUrlStopIndicator Specifies value for NextUrlAttribute or StopIndicatorAttribute which indicates last page to stop pagination. If you have specified StopIndicatorAttribute then you can use Regular expression rather than static value to indicate last page. To use regular expression value of this property must start with regex= prefix. Example : regex=FALSE|N  (assuming you set StopIndicatorAttribute to something like $.hasMore)
StopIndicatorAttribute Attribute name or expression for attribute which can be used as stop indicator (e.g. hasmore --or-- $.pagination.hasmore). If this value is blank then NextUrlAttribute is used
NextUrlSuffix If you want to include certain text (or parameters) at the end of Next url then specify this attribute (e.g. &format=json). Another common use case of this property is to supply pagination token to next Page URL. You can also use <%nextlink%> or  <%nextlink_encoded%> placeholder (e.g. &cursor=<%nextlink_encoded%> )
NextUrlWait This property indicates total number of milliseconds you want to wait before sending next request. This option allows you to adjust how many API calls can be made within certain timeframe. If your API Service has no limit then set this option to zero
ContinueOnUrlNotFoundError If this option is true then component will continue without exception on 404 error (Url not found). This allows you to consume data gracefully.
ContineOnAnyError Continue when any type of exception occurs during http request
ContineOnErrorForMessage Continue on error when specified substring found in response
ContineOnErrorForStatusCode Continue on error when specified status code returned from web server
ConsumeResponseOnError When error occurs no data is returned. Use this option to get content eventhough error occurs. When this option is checked you can't use [continue on error when specific string found in response] option
ErrorStatusCodeToMatch Status code to match when error occurs and ContineOnErrorForStatusCode option is true. If Response status code matches to this code then task continues to run
ErrorSubstringToMatch Error substring to match when error occurs and ContineOnErrorForMessage option is true. If Response status code matches to this code then task continues to run
CookieContainerVariable Cookie Container can be used to maintain state between multiple web requests. Example: You can login to site like wordpress and then extract any private page content by simply passing authentication cookies using this variable.
RequestTimeout Http request Timeout in seconds. Set this to 0 if you want to use system default value (i.e. 100 seconds)
SecurityProtocol Specifies which security protocol is supported for HTTPS communication. Using this option you can enable legacy protocol or enforce to use latest version of security protocol (Note: TLS 1.2 is only supported in SSIS 2014 or Higher).

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
Default [0] System Default
Ssl3 [1] SSL v3.0
Ssl3Plus [2] SSL v3.0 or higher
Tls [3] TLS v1.0
TlsPlus [4] TLS v1.0 or higher
Tls11 [5] TLS v1.1
Tls11Plus [6] TLS v1.1 or higher
Tls12 [7] TLS v1.2
Tls12Plus [8] TLS v1.2 or higher
EnableCompressionSupport Enable support for gzip or deflate compression. When you check this option compressed response automatically de-compressed saving bandwidth. This option is only valid if web server supports compressed response stream. Check your API documentation for more information.
IgnoreCertificateErrors Ignore SSL certificate related errors. Example: if you getting SSL/TLS errors because of certificate expired or certificate is not from trusted authority or certificate is self-signed. By checking this option you will not get SSL/TLS error.
AllowUnsecureSuite Allow unsecure ciphers/suites and curves for SSLS/TLS communication. Use this option to communicate with web servers which needs legacy / unsecured ciphers support. This option is only trigged when you change default SSL/TLS Version on advanced settings tab.
UseConnection Use connection to pass credentials for authentication (e.g. Use UserID/Password or Use OAuth Protocol for token based approach)
PagingMode Specified how you want to loop through multiple pages returned by REST API.

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
ByResponseAttribute [0] Response Attribute Mode - Read next page information from response
ByUrlParameter [1] Url Parameter Mode - Page number is passed as query string parameter
ByUrlPath [2] Url Path Mode - Page number is passed as URL path
ByPostData [3] POST data Mode - Page number is passed inside POST data
ByUrlParameterMulti [4] Url Parameter Mode (Multi) - Pass Start and End Row Number in URL
ByResponseHeaderRfc5988 [5] Response Header contains Next Link - RFC 5988 (Next URL Link found in Standard Header)
ByResponseHeaderCustom [6] Response Header contains Next Link - Custom (Next URL Link found in Custom Header)
ByResponseHeaderContinuationToken [7] Response Header contains Continuation Token
EnablePageTokenForBody If you wish to pass extracted pagination token or current page number in the body of next request then set this option to true. You can use [$pagetoken$] and [$pagenumber$] placeholders anywhere in the Body where you wish to insert extracted Page token. If you must set encoded token then you can use <%nextlink_encoded%> inside SuffixForNextUrl Property. If you dont use SuffixForNextUrl then raw nextLink or Token will be inserted inside the body. If you dont specify [$pagetoken$] placeholder in the body then NextPage Token will be appended at the end. NextPage Token is extracted by filter expression specified using property NextUrlSuffix
HasDifferentNextPageInfo Set this to true if you wish to specify different URL, Header, Body or Filter for first page and next page (i.e. Paginated response). Some APIs like Amazon MWS, NetSuite, Zuora) you may need to set this to true.
PagePlaceholders When HasDifferentNextPageInfo=true you can set this property to indicate first page and next page. You can specify different URL, Header, Body or Filter for first page and next page (i.e. Paginated response). Use [$tag$] as placeholder anywhere in the URL, Header, Body or Filter and at runtime system will replace it with correct value (first page or next page value). Syntax to specify placeholder for first page vs next page is like a connectionstring url=FirstPageValue|NextPageValue;header=FirstPageValue|NextPageValue;body=FirstPageValue|NextPageValue;filter=FirstPageValue|NextPageValue; You can use one or more key/value pairs to make things dynamic (e.g. url, header, body or filter)  .For example if you have pagination in your API and First URL is http://abc.com/api/items/get and to get more records you have to call http://abc.com/api/items/getNext then you can use [$tag$] as placeholder in the URL http://abc.com/api/items/[$tag$] and specify this property with first page tag and next page tag as url=get|getNext  (Tags are separated using vertical bar).
FirstPageBodyPart Use this property to set request body fragment for first page. HasDifferentNextPageInfo must be set to true to use this property.
NextPageBodyPart Use this property to set request body fragment for any request after first request. HasDifferentNextPageInfo must be set to true to use this property.
PagingMaxPagesExpr Expression to extract Maximum pages to loop through. Some APIs don't stop pagination and keep returning last page data when you try to read data after last page. Specify expression (e.g. $.page_count ) to read total pages to loop through using this property.
PagingMaxRowsExpr Expression to extract Maximum records to loop through. Some APIs don't stop pagination and keep returning last page data when you try to read data after last page. Specify expression (e.g. $.total_rows ) to read total pages to loop through using this property. This setting is ignored if you set PagingMaxPagesExpr.
PagingMaxRowsDataPathExpr When you enable PagingMaxRowsExpr (end pagination based on MaxRowCount) then you need to count records coming in each response. This expression extract all rows found under specified expression (e.g. $.orders[*] if all records found under orders node).
PageNumberAttributeNameInUrl e.g. Type page_num if URL looks like this => http://abc.com/?page_num=1&sort=true  (page number via query string)
--or-- Type <%page%> if page number is inside URL path like this => http://abc.com/1/?sort=true  (e.g. replace page number in url with placeholder http://abc.com/<%page%>/?sort=true)
Page number will be incremented by one for next URL until last page is reached or [Max Page Number] is reached
MaxPageNumber Maximum page number until which auto increment is allowed. Type zero for no limit. Next URL contains next page number (increment by one) until last page is detected or [Max Page Number] limit is reached.
PagingEndRules Rules to end pagination. You can use XML markup to include multiple rules. Here is an example of XML with multiple rules. This will stop pagination if any of these rule matches (Status Code, Size, Error Message)  <ArrayOfPagingEndRule><PagingEndRule><Mode>DetectBasedOnResponseStatusCode</Mode><StatusCode>401</StatusCode></PagingEndRule><PagingEndRule><Mode>DetectBasedOnRecordCount</Mode></PagingEndRule><PagingEndRule><Mode>DetectBasedOnResponseSize</Mode><MinBytes>3</MinBytes><MaxBytes>200</MaxBytes></PagingEndRule><PagingEndRule><Mode>DetectBasedOnResponseErrorMessage</Mode><ErrString>key not found</ErrString></PagingEndRule></ArrayOfPagingEndRule>
StartPageNumberVariable Variable name which will hold starting page number. This is ignored if you using parameter name from query string to indicate page number.
PageNumberIncrement Page counter increment. By default next page is incremented by one if this value is zero. You can also enter negative number if you want to decrease page counter.
PagingEndStrategy Specified how you want detect last page.

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
DetectBasedOnResponseSize [0] Detect last page based on response size (in bytes)
DetectBasedOnResponseErrorMessage [1] Detect last page based on error message (sub string)
DetectBasedOnResponseStatusCode [2] Detect last page based on status code (numeric code)
DetectBasedOnRecordCount [3] Detect based on missing row (stop when no more records)
DetectBasedOnMultipleRules [4] Detect based on multiple rules (i.e. mix of status(es), size, error)
LastPageWhenConditionEqualsTo Condition result to compare to detect last page. Set this property to True if you want detect last page if condition is true else set this to False.
ResponseMinBytes Minimum bytes expected from response.
ResponseMaxBytes Maximum bytes from response.
ResponseErrorString Expected error message sub string from response.
ResponseStatusCode Expected status code from response when page number you trying to access not found.
MaxRows Maximum JSON records to fetch. Set this value to 0 for all records
FileCompressionType Compression format for source file (e.g. gzip, zip)

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
None [0] None
GZip [1] GZip
Zip [2] Zip
DateFormatString Specifies how custom date formatted strings are parsed when reading JSON.
DateParseHandling Specifies how date formatted strings, e.g. Date(1198908717056) and 2012-03-21T05:40Z, are parsed when reading JSON.

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
None [0] Keep date as string
DateTime [1] Convert to DateTime (Timezone lost)
DateTimeOffset [2] Convert to DateTimeOffset (Preserve Time zone)
FloatParseHandling Specifies how decimal values are parsed when reading JSON. Change this setting to Decimal if you like to have large precision / scale.

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
Double [0] Default (Double [~15-17 digits])
Decimal [1] Decimal (High Precision / Scale [~28-29 digits] )
OutputFilePath Set this option to true if you want to output FilePath. This option is ignored when you consume DirectValue or data from Url rather than local files. Output column name will be __FilePath
OutputFileName Set this option to true if you want to output FileName. This option is ignored when you consume DirectValue or data from Url rather than local files. Output column name will be __FileName
EnableArchiveFile Set this option to true if you want to move processed file to archive folder.
ArchiveFolderPath Folder path where you want to move processed file.
OverwriteFileInArchiveFolder Folder path where you want to move processed file.
ArchiveFileNamingConvention File naming convention for archived file. By default it will same name as original source file processed. But you can control naming format using {%name%} and {%ext%} placeholders. e.g. {%name%}_processed{%ext%} or {%name%}{%ext%}.{{System::ContainerStartTime,yyyyMMdd_HHmmss_fff}}
EnablePivot When this property is true then Column is converted to Row. Pivoted names will appear under  Pivot_Name column and values will appear under Pivot_Value field.
MetaDataScanMode Metadata scan mode controls how data type and length is determined. By default few records scanned to determine datatype/length. Changing ScanMode affects length/datatype accuracy.

Available Options (Use numeric value listed in bracket if you have to define expression on this property (for dynamic behavior).

Option Description
Auto [0] Auto
Strict [1] Strict - Exact length
Guess2x [2] Guess2x - 2 times bigger
Guess3x [3] Guess3x - 3 times bigger
Guess4x [4] Guess4x - 4 times bigger
TreatAsUnicodeString [5] Treat all columns as string
MetaDataCustomLength Length for all string column. This option is only valid for MetaDataScanMode=Custom

Setting UI


SSIS CSV Source (Web API or File) - Setting UI
SSIS CSV Source (Web API or File) - Setting UI
SSIS CSV Source (Web API or File) - Setting UI
SSIS CSV Source (Web API or File) - Setting UI

See Also

Articles / Tutorials

Click here to see all articles for [SSIS CSV Source] category
How to Pivot CSV Data in SSIS

How to Pivot CSV Data in SSIS

Introduction In our previous blog we saw How to write data into CSV file in SSIS (GZip / Split). Now in this blog, we will see How to Pivot CSV Data in SSIS using CSV Source. It also supports Pivot mode so you can convert single CSV string value into Rows. In this article we […]


SSIS Magento data Read / Write using REST API Call

SSIS Magento data Read / Write using REST API Call

Introduction In this post we will lean SSIS Magento data read / write operations. Magento is a very popular eCommerce platform and they offer JSON based REST API and XML based SOAP API. You can use either API based on your need to automate common integration needs. We recommend using REST API (JSON API) if possible […]


How to call Amazon MWS API using SSIS

How to call Amazon MWS API using SSIS

Introduction In this post you will learn how to call Amazon MWS API (Amazon Marketplace Web Service) or Amazon Product Advertising API using SSIS PowerPack. Using drag and drop approach you can consume data from Amazon MWS XML Web service. In this post we will use ZappySys XML Source connector to read data from Amazon MWS […]



Copyrights reserved. ZappySys LLC.