...

Source file src/github.com/Azure/azure-sdk-for-go/storage/blob_test.go

Documentation: github.com/Azure/azure-sdk-for-go/storage

     1  package storage
     2  
     3  // Copyright (c) Microsoft Corporation. All rights reserved.
     4  // Licensed under the MIT License. See License.txt in the project root for license information.
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/xml"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"strconv"
    13  	"strings"
    14  
    15  	chk "gopkg.in/check.v1"
    16  )
    17  
    18  type StorageBlobSuite struct{}
    19  
    20  var _ = chk.Suite(&StorageBlobSuite{})
    21  
    22  func getBlobClient(c *chk.C) BlobStorageClient {
    23  	return getBasicClient(c).GetBlobService()
    24  }
    25  
    26  func (s *StorageBlobSuite) Test_buildPath(c *chk.C) {
    27  	cli := getBlobClient(c)
    28  	cnt := cli.GetContainerReference("lol")
    29  	b := cnt.GetBlobReference("rofl")
    30  	c.Assert(b.buildPath(), chk.Equals, "/lol/rofl")
    31  }
    32  
    33  func (s *StorageBlobSuite) Test_pathForResource(c *chk.C) {
    34  	c.Assert(pathForResource("lol", ""), chk.Equals, "/lol")
    35  	c.Assert(pathForResource("lol", "blob"), chk.Equals, "/lol/blob")
    36  }
    37  
    38  func (s *StorageBlobSuite) TestBlobExists(c *chk.C) {
    39  	cli := getBlobClient(c)
    40  	rec := cli.client.appendRecorder(c)
    41  	defer rec.Stop()
    42  
    43  	cnt := cli.GetContainerReference(containerName(c))
    44  	c.Assert(cnt.Create(nil), chk.IsNil)
    45  	b := cnt.GetBlobReference(blobName(c))
    46  	defer cnt.Delete(nil)
    47  
    48  	c.Assert(b.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
    49  	defer b.Delete(nil)
    50  
    51  	ok, err := b.Exists()
    52  	c.Assert(err, chk.IsNil)
    53  	c.Assert(ok, chk.Equals, true)
    54  	b.Name += ".lol"
    55  	ok, err = b.Exists()
    56  	c.Assert(err, chk.IsNil)
    57  	c.Assert(ok, chk.Equals, false)
    58  
    59  }
    60  
    61  func (s *StorageBlobSuite) TestGetBlobURL(c *chk.C) {
    62  	cli, err := NewBasicClient(dummyStorageAccount, dummyMiniStorageKey)
    63  	c.Assert(err, chk.IsNil)
    64  	blobCli := cli.GetBlobService()
    65  
    66  	cnt := blobCli.GetContainerReference("c")
    67  	b := cnt.GetBlobReference("nested/blob")
    68  	c.Assert(b.GetURL(), chk.Equals, "https://golangrocksonazure.blob.core.windows.net/c/nested/blob")
    69  
    70  	cnt.Name = ""
    71  	c.Assert(b.GetURL(), chk.Equals, "https://golangrocksonazure.blob.core.windows.net/$root/nested/blob")
    72  
    73  	b.Name = "blob"
    74  	c.Assert(b.GetURL(), chk.Equals, "https://golangrocksonazure.blob.core.windows.net/$root/blob")
    75  
    76  }
    77  
    78  func (s *StorageBlobSuite) TestGetBlobContainerURL(c *chk.C) {
    79  	cli, err := NewBasicClient(dummyStorageAccount, dummyMiniStorageKey)
    80  	c.Assert(err, chk.IsNil)
    81  	blobCli := cli.GetBlobService()
    82  
    83  	cnt := blobCli.GetContainerReference("c")
    84  	b := cnt.GetBlobReference("")
    85  	c.Assert(b.GetURL(), chk.Equals, "https://golangrocksonazure.blob.core.windows.net/c")
    86  
    87  	cnt.Name = ""
    88  	c.Assert(b.GetURL(), chk.Equals, "https://golangrocksonazure.blob.core.windows.net/$root")
    89  }
    90  
    91  func (s *StorageBlobSuite) TestDeleteBlobIfExists(c *chk.C) {
    92  	cli := getBlobClient(c)
    93  	rec := cli.client.appendRecorder(c)
    94  	defer rec.Stop()
    95  
    96  	cnt := cli.GetContainerReference(containerName(c))
    97  	b := cnt.GetBlobReference(blobName(c))
    98  	c.Assert(cnt.Create(nil), chk.IsNil)
    99  	defer cnt.Delete(nil)
   100  
   101  	c.Assert(b.Delete(nil), chk.NotNil)
   102  
   103  	ok, err := b.DeleteIfExists(nil)
   104  	c.Assert(err, chk.IsNil)
   105  	c.Assert(ok, chk.Equals, false)
   106  }
   107  
   108  func (s *StorageBlobSuite) TestDeleteBlobWithConditions(c *chk.C) {
   109  	cli := getBlobClient(c)
   110  	rec := cli.client.appendRecorder(c)
   111  	defer rec.Stop()
   112  
   113  	cnt := cli.GetContainerReference(containerName(c))
   114  	b := cnt.GetBlobReference(blobName(c))
   115  	c.Assert(cnt.Create(nil), chk.IsNil)
   116  	defer cnt.Delete(nil)
   117  
   118  	c.Assert(b.CreateBlockBlob(nil), chk.IsNil)
   119  	err := b.GetProperties(nil)
   120  	c.Assert(err, chk.IsNil)
   121  	etag := b.Properties.Etag
   122  
   123  	// "Delete if matches incorrect or old Etag" should fail without deleting.
   124  	options := DeleteBlobOptions{
   125  		IfMatch: "GolangRocksOnAzure",
   126  	}
   127  	err = b.Delete(&options)
   128  	c.Assert(err, chk.FitsTypeOf, AzureStorageServiceError{})
   129  	c.Assert(err.(AzureStorageServiceError).StatusCode, chk.Equals, http.StatusPreconditionFailed)
   130  	ok, err := b.Exists()
   131  	c.Assert(err, chk.IsNil)
   132  	c.Assert(ok, chk.Equals, true)
   133  
   134  	// "Delete if matches new Etag" should succeed.
   135  	options.IfMatch = etag
   136  	ok, err = b.DeleteIfExists(&options)
   137  	c.Assert(err, chk.IsNil)
   138  	c.Assert(ok, chk.Equals, true)
   139  }
   140  
   141  func (s *StorageBlobSuite) TestGetBlobProperties(c *chk.C) {
   142  	cli := getBlobClient(c)
   143  	rec := cli.client.appendRecorder(c)
   144  	defer rec.Stop()
   145  
   146  	cnt := cli.GetContainerReference(containerName(c))
   147  	c.Assert(cnt.Create(nil), chk.IsNil)
   148  	defer cnt.Delete(nil)
   149  
   150  	// try to get properties on a nonexisting blob
   151  	blob1 := cnt.GetBlobReference(blobName(c, "1"))
   152  	err := blob1.GetProperties(nil)
   153  	c.Assert(err, chk.NotNil)
   154  
   155  	// Put a blob
   156  	blob2 := cnt.GetBlobReference(blobName(c, "2"))
   157  	contents := content(64)
   158  	c.Assert(blob2.putSingleBlockBlob(contents), chk.IsNil)
   159  
   160  	// Get blob properties
   161  	err = blob2.GetProperties(nil)
   162  	c.Assert(err, chk.IsNil)
   163  
   164  	c.Assert(blob2.Properties.ContentLength, chk.Equals, int64(len(contents)))
   165  	c.Assert(blob2.Properties.ContentType, chk.Equals, "application/octet-stream")
   166  	c.Assert(blob2.Properties.BlobType, chk.Equals, BlobTypeBlock)
   167  }
   168  
   169  // Ensure it's possible to generate a ListBlobs response with
   170  // metadata, e.g., for a stub server.
   171  func (s *StorageBlobSuite) TestMarshalBlobMetadata(c *chk.C) {
   172  	buf, err := xml.Marshal(Blob{
   173  		Name:       blobName(c),
   174  		Properties: BlobProperties{},
   175  		Metadata: map[string]string{
   176  			"lol": "baz < waz",
   177  		},
   178  	})
   179  	c.Assert(err, chk.IsNil)
   180  	c.Assert(string(buf), chk.Matches, `.*<Metadata><Lol>baz &lt; waz</Lol></Metadata>.*`)
   181  }
   182  
   183  func (s *StorageBlobSuite) TestGetAndSetBlobMetadata(c *chk.C) {
   184  	cli := getBlobClient(c)
   185  	rec := cli.client.appendRecorder(c)
   186  	defer rec.Stop()
   187  
   188  	cnt := cli.GetContainerReference(containerName(c))
   189  	c.Assert(cnt.Create(nil), chk.IsNil)
   190  	defer cnt.Delete(nil)
   191  
   192  	// Get empty metadata
   193  	blob1 := cnt.GetBlobReference(blobName(c, "1"))
   194  	c.Assert(blob1.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   195  
   196  	err := blob1.GetMetadata(nil)
   197  	c.Assert(err, chk.IsNil)
   198  	c.Assert(blob1.Metadata, chk.HasLen, 0)
   199  
   200  	// Get and set the metadata
   201  	blob2 := cnt.GetBlobReference(blobName(c, "2"))
   202  	c.Assert(blob2.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   203  	metaPut := BlobMetadata{
   204  		"lol":      "rofl",
   205  		"rofl_baz": "waz qux",
   206  	}
   207  	blob2.Metadata = metaPut
   208  
   209  	err = blob2.SetMetadata(nil)
   210  	c.Assert(err, chk.IsNil)
   211  
   212  	err = blob2.GetMetadata(nil)
   213  	c.Assert(err, chk.IsNil)
   214  	c.Check(blob2.Metadata, chk.DeepEquals, metaPut)
   215  }
   216  
   217  func (s *StorageBlobSuite) TestMetadataCaseMunging(c *chk.C) {
   218  	cli := getBlobClient(c)
   219  	rec := cli.client.appendRecorder(c)
   220  	defer rec.Stop()
   221  
   222  	cnt := cli.GetContainerReference(containerName(c))
   223  	c.Assert(cnt.Create(nil), chk.IsNil)
   224  	defer cnt.Delete(nil)
   225  
   226  	b := cnt.GetBlobReference(blobName(c))
   227  	c.Assert(b.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   228  
   229  	// Case munging
   230  	metaPutUpper := BlobMetadata{
   231  		"Lol":      "different rofl",
   232  		"rofl_BAZ": "different waz qux",
   233  	}
   234  	metaExpectLower := BlobMetadata{
   235  		"lol":      "different rofl",
   236  		"rofl_baz": "different waz qux",
   237  	}
   238  
   239  	b.Metadata = metaPutUpper
   240  	err := b.SetMetadata(nil)
   241  	c.Assert(err, chk.IsNil)
   242  
   243  	err = b.GetMetadata(nil)
   244  	c.Assert(err, chk.IsNil)
   245  	c.Check(b.Metadata, chk.DeepEquals, metaExpectLower)
   246  }
   247  
   248  func (s *StorageBlobSuite) TestSetMetadataWithExtraHeaders(c *chk.C) {
   249  	cli := getBlobClient(c)
   250  	rec := cli.client.appendRecorder(c)
   251  	defer rec.Stop()
   252  
   253  	cnt := cli.GetContainerReference(containerName(c))
   254  	b := cnt.GetBlobReference(blobName(c))
   255  	c.Assert(cnt.Create(nil), chk.IsNil)
   256  	defer cnt.Delete(nil)
   257  
   258  	c.Assert(b.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   259  
   260  	meta := BlobMetadata{
   261  		"lol":      "rofl",
   262  		"rofl_baz": "waz qux",
   263  	}
   264  	b.Metadata = meta
   265  
   266  	options := SetBlobMetadataOptions{
   267  		IfMatch: "incorrect-etag",
   268  	}
   269  
   270  	// Set with incorrect If-Match in extra headers should result in error
   271  	err := b.SetMetadata(&options)
   272  	c.Assert(err, chk.NotNil)
   273  
   274  	err = b.GetProperties(nil)
   275  	c.Assert(err, chk.IsNil)
   276  
   277  	// Set with matching If-Match in extra headers should succeed
   278  	options.IfMatch = b.Properties.Etag
   279  	b.Metadata = meta
   280  	err = b.SetMetadata(&options)
   281  	c.Assert(err, chk.IsNil)
   282  }
   283  
   284  func (s *StorageBlobSuite) TestSetBlobProperties(c *chk.C) {
   285  	cli := getBlobClient(c)
   286  	rec := cli.client.appendRecorder(c)
   287  	defer rec.Stop()
   288  
   289  	cnt := cli.GetContainerReference(containerName(c))
   290  	b := cnt.GetBlobReference(blobName(c))
   291  	c.Assert(cnt.Create(nil), chk.IsNil)
   292  	defer cnt.Delete(nil)
   293  
   294  	c.Assert(b.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   295  
   296  	input := BlobProperties{
   297  		CacheControl:    "private, max-age=0, no-cache",
   298  		ContentMD5:      "oBATU+oaDduHWbVZLuzIJw==",
   299  		ContentType:     "application/json",
   300  		ContentEncoding: "gzip",
   301  		ContentLanguage: "de-DE",
   302  	}
   303  	b.Properties = input
   304  
   305  	err := b.SetProperties(nil)
   306  	c.Assert(err, chk.IsNil)
   307  
   308  	err = b.GetProperties(nil)
   309  	c.Assert(err, chk.IsNil)
   310  
   311  	c.Check(b.Properties.CacheControl, chk.Equals, input.CacheControl)
   312  	c.Check(b.Properties.ContentType, chk.Equals, input.ContentType)
   313  	c.Check(b.Properties.ContentMD5, chk.Equals, input.ContentMD5)
   314  	c.Check(b.Properties.ContentEncoding, chk.Equals, input.ContentEncoding)
   315  	c.Check(b.Properties.ContentLanguage, chk.Equals, input.ContentLanguage)
   316  }
   317  
   318  func (s *StorageBlobSuite) TestSetPageBlobProperties(c *chk.C) {
   319  	cli := getBlobClient(c)
   320  	rec := cli.client.appendRecorder(c)
   321  	defer rec.Stop()
   322  
   323  	cnt := cli.GetContainerReference(containerName(c))
   324  	b := cnt.GetBlobReference(blobName(c))
   325  	c.Assert(cnt.Create(nil), chk.IsNil)
   326  	defer cnt.Delete(nil)
   327  
   328  	size := int64(1024)
   329  	b.Properties.ContentLength = size
   330  	c.Assert(b.PutPageBlob(nil), chk.IsNil)
   331  
   332  	b.Properties.ContentLength = int64(512)
   333  	options := SetBlobPropertiesOptions{Timeout: 30}
   334  	err := b.SetProperties(&options)
   335  	c.Assert(err, chk.IsNil)
   336  }
   337  
   338  func (s *StorageBlobSuite) TestSnapshotBlob(c *chk.C) {
   339  	cli := getBlobClient(c)
   340  	rec := cli.client.appendRecorder(c)
   341  	defer rec.Stop()
   342  
   343  	cnt := cli.GetContainerReference(containerName(c))
   344  	b := cnt.GetBlobReference(blobName(c))
   345  	c.Assert(cnt.Create(nil), chk.IsNil)
   346  	defer cnt.Delete(nil)
   347  
   348  	c.Assert(b.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   349  
   350  	snapshotTime, err := b.CreateSnapshot(nil)
   351  	c.Assert(err, chk.IsNil)
   352  	c.Assert(snapshotTime, chk.NotNil)
   353  }
   354  
   355  func (s *StorageBlobSuite) TestSnapshotBlobWithTimeout(c *chk.C) {
   356  	cli := getBlobClient(c)
   357  	rec := cli.client.appendRecorder(c)
   358  	defer rec.Stop()
   359  
   360  	cnt := cli.GetContainerReference(containerName(c))
   361  	b := cnt.GetBlobReference(blobName(c))
   362  	c.Assert(cnt.Create(nil), chk.IsNil)
   363  	defer cnt.Delete(nil)
   364  
   365  	c.Assert(b.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   366  
   367  	options := SnapshotOptions{
   368  		Timeout: 0,
   369  	}
   370  	snapshotTime, err := b.CreateSnapshot(&options)
   371  	c.Assert(err, chk.IsNil)
   372  	c.Assert(snapshotTime, chk.NotNil)
   373  }
   374  
   375  func (s *StorageBlobSuite) TestSnapshotBlobWithValidLease(c *chk.C) {
   376  	cli := getBlobClient(c)
   377  	rec := cli.client.appendRecorder(c)
   378  	defer rec.Stop()
   379  
   380  	cnt := cli.GetContainerReference(containerName(c))
   381  	b := cnt.GetBlobReference(blobName(c))
   382  	c.Assert(cnt.Create(nil), chk.IsNil)
   383  	defer cnt.Delete(nil)
   384  
   385  	c.Assert(b.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   386  
   387  	// generate lease.
   388  	currentLeaseID, err := b.AcquireLease(30, "", nil)
   389  	c.Assert(err, chk.IsNil)
   390  
   391  	options := SnapshotOptions{
   392  		LeaseID: currentLeaseID,
   393  	}
   394  	snapshotTime, err := b.CreateSnapshot(&options)
   395  	c.Assert(err, chk.IsNil)
   396  	c.Assert(snapshotTime, chk.NotNil)
   397  }
   398  
   399  func (s *StorageBlobSuite) TestSnapshotBlobWithInvalidLease(c *chk.C) {
   400  	cli := getBlobClient(c)
   401  	rec := cli.client.appendRecorder(c)
   402  	defer rec.Stop()
   403  
   404  	cnt := cli.GetContainerReference(containerName(c))
   405  	b := cnt.GetBlobReference(blobName(c))
   406  	c.Assert(cnt.Create(nil), chk.IsNil)
   407  	defer cnt.Delete(nil)
   408  
   409  	c.Assert(b.putSingleBlockBlob([]byte("Hello!")), chk.IsNil)
   410  
   411  	// generate lease.
   412  	leaseID, err := b.AcquireLease(30, "", nil)
   413  	c.Assert(err, chk.IsNil)
   414  	c.Assert(leaseID, chk.Not(chk.Equals), "")
   415  
   416  	options := SnapshotOptions{
   417  		LeaseID: "GolangRocksOnAzure",
   418  	}
   419  	snapshotTime, err := b.CreateSnapshot(&options)
   420  	c.Assert(err, chk.NotNil)
   421  	c.Assert(snapshotTime, chk.IsNil)
   422  }
   423  
   424  func (s *StorageBlobSuite) TestGetBlobRange(c *chk.C) {
   425  	cli := getBlobClient(c)
   426  	rec := cli.client.appendRecorder(c)
   427  	defer rec.Stop()
   428  
   429  	cnt := cli.GetContainerReference(containerName(c))
   430  	b := cnt.GetBlobReference(blobName(c))
   431  	c.Assert(cnt.Create(nil), chk.IsNil)
   432  	defer cnt.Delete(nil)
   433  
   434  	body := "0123456789"
   435  	c.Assert(b.putSingleBlockBlob([]byte(body)), chk.IsNil)
   436  	defer b.Delete(nil)
   437  
   438  	cases := []struct {
   439  		options  GetBlobRangeOptions
   440  		expected string
   441  	}{
   442  		{
   443  			options: GetBlobRangeOptions{
   444  				Range: &BlobRange{
   445  					Start: 0,
   446  					End:   uint64(len(body)),
   447  				},
   448  			},
   449  			expected: body,
   450  		},
   451  		{
   452  			options: GetBlobRangeOptions{
   453  				Range: &BlobRange{
   454  					Start: 0,
   455  					End:   0,
   456  				},
   457  			},
   458  			expected: body,
   459  		},
   460  		{
   461  			options: GetBlobRangeOptions{
   462  				Range: &BlobRange{
   463  					Start: 1,
   464  					End:   3,
   465  				},
   466  			},
   467  			expected: body[1 : 3+1],
   468  		},
   469  		{
   470  			options: GetBlobRangeOptions{
   471  				Range: &BlobRange{
   472  					Start: 3,
   473  					End:   uint64(len(body)),
   474  				},
   475  			},
   476  			expected: body[3:],
   477  		},
   478  		{
   479  			options: GetBlobRangeOptions{
   480  				Range: &BlobRange{
   481  					Start: 3,
   482  					End:   0,
   483  				},
   484  			},
   485  			expected: body[3:],
   486  		},
   487  	}
   488  
   489  	err := b.GetProperties(nil)
   490  	c.Assert(err, chk.IsNil)
   491  
   492  	// Read 1-3
   493  	for _, r := range cases {
   494  		resp, err := b.GetRange(&(r.options))
   495  		c.Assert(err, chk.IsNil)
   496  		blobBody, err := ioutil.ReadAll(resp)
   497  		c.Assert(err, chk.IsNil)
   498  
   499  		str := string(blobBody)
   500  		c.Assert(str, chk.Equals, r.expected)
   501  
   502  		// Was content length left untouched?
   503  		c.Assert(b.Properties.ContentLength, chk.Equals, int64(len(body)))
   504  	}
   505  }
   506  
   507  func (b *Blob) putSingleBlockBlob(chunk []byte) error {
   508  	if len(chunk) > MaxBlobBlockSize {
   509  		return fmt.Errorf("storage: provided chunk (%d bytes) cannot fit into single-block blob (max %d bytes)", len(chunk), MaxBlobBlockSize)
   510  	}
   511  
   512  	uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), nil)
   513  	headers := b.Container.bsc.client.getStandardHeaders()
   514  	b.Properties.BlobType = BlobTypeBlock
   515  	headers["x-ms-blob-type"] = string(BlobTypeBlock)
   516  	headers["Content-Length"] = strconv.Itoa(len(chunk))
   517  
   518  	resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, bytes.NewReader(chunk), b.Container.bsc.auth)
   519  	if err != nil {
   520  		return err
   521  	}
   522  	return checkRespCode(resp, []int{http.StatusCreated})
   523  }
   524  
   525  func blobName(c *chk.C, extras ...string) string {
   526  	return nameGenerator(1024, "blob/", alphanum, c, extras)
   527  
   528  }
   529  
   530  func contentWithSpecialChars(n int) string {
   531  	name := string(content(n)) + "/" + string(content(n)) + "-._~:?#[]@!$&'()*,;+= " + string(content(n))
   532  	return name
   533  }
   534  
   535  func nameGenerator(maxLen int, prefix, valid string, c *chk.C, extras []string) string {
   536  	extra := strings.Join(extras, "")
   537  	name := prefix + extra + removeInvalidCharacters(c.TestName(), valid)
   538  	if len(name) > maxLen {
   539  		return name[:maxLen]
   540  	}
   541  	return name
   542  }
   543  
   544  func removeInvalidCharacters(unformatted string, valid string) string {
   545  	unformatted = strings.ToLower(unformatted)
   546  	buffer := bytes.NewBufferString(strconv.Itoa((len(unformatted))))
   547  	runes := []rune(unformatted)
   548  	for _, r := range runes {
   549  		if strings.ContainsRune(valid, r) {
   550  			buffer.WriteRune(r)
   551  		}
   552  	}
   553  	return string(buffer.Bytes())
   554  }
   555  
   556  func content(n int) []byte {
   557  	buffer := bytes.NewBufferString("")
   558  	rep := (n / len(veryLongString)) + 1
   559  	for i := 0; i < rep; i++ {
   560  		buffer.WriteString(veryLongString)
   561  	}
   562  	return buffer.Bytes()[:n]
   563  }
   564  
   565  const (
   566  	alphanum       = "0123456789abcdefghijklmnopqrstuvwxyz"
   567  	alpha          = "abcdefghijklmnopqrstuvwxyz"
   568  	veryLongString = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer feugiat eleifend scelerisque. Phasellus tempor turpis eget magna pretium, et finibus massa convallis. Donec eget lacinia nibh. Ut ut cursus odio. Quisque id justo interdum, maximus ex a, dapibus leo. Nullam mattis arcu nec justo vehicula pretium. Curabitur fermentum quam ac dolor venenatis, vitae scelerisque ex posuere. Donec ut ante porttitor, ultricies ante ac, pulvinar metus. Nunc suscipit elit gravida dolor facilisis sollicitudin. Fusce ac ultrices libero. Donec erat lectus, hendrerit volutpat nisl quis, porta accumsan nibh. Pellentesque hendrerit nisi id mi porttitor maximus. Phasellus vitae venenatis velit. Quisque id felis nec lacus iaculis porttitor. Maecenas egestas tortor et nulla dapibus varius. In hac habitasse platea dictumst."
   569  )
   570  

View as plain text